From b0de113ad2830a666f049aba2f526f30d9346ec8 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 02:38:04 +0100 Subject: [PATCH 01/38] New configuration format --- .../Configuration/AccountConfigurationV7.cs | 80 +++++++++++ Pal.Client/Configuration/ConfigurationData.cs | 7 + .../Configuration/ConfigurationManager.cs | 108 +++++++++++++++ .../ConfigurationV1.cs} | 111 ++------------- Pal.Client/Configuration/ConfigurationV7.cs | 48 +++++++ Pal.Client/Configuration/EMode.cs | 15 +++ Pal.Client/Configuration/ERenderer.cs | 11 ++ .../Configuration/IPalacePalConfiguration.cs | 89 +++++++++++++ Pal.Client/Net/RemoteApi.AccountService.cs | 41 +++--- Pal.Client/Net/RemoteApi.Utils.cs | 2 +- Pal.Client/Net/RemoteApi.cs | 25 +--- Pal.Client/Plugin.cs | 53 +++++--- Pal.Client/Rendering/SimpleRenderer.cs | 2 +- Pal.Client/Scheduled/QueuedImport.cs | 5 +- Pal.Client/Scheduled/QueuedSyncResponse.cs | 4 +- Pal.Client/Service.cs | 4 +- Pal.Client/Windows/AgreementWindow.cs | 2 +- Pal.Client/Windows/ConfigWindow.cs | 126 ++++++++++-------- 18 files changed, 510 insertions(+), 223 deletions(-) create mode 100644 Pal.Client/Configuration/AccountConfigurationV7.cs create mode 100644 Pal.Client/Configuration/ConfigurationData.cs create mode 100644 Pal.Client/Configuration/ConfigurationManager.cs rename Pal.Client/{Configuration.cs => Configuration/ConfigurationV1.cs} (60%) create mode 100644 Pal.Client/Configuration/ConfigurationV7.cs create mode 100644 Pal.Client/Configuration/EMode.cs create mode 100644 Pal.Client/Configuration/ERenderer.cs create mode 100644 Pal.Client/Configuration/IPalacePalConfiguration.cs diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs new file mode 100644 index 0000000..beb8695 --- /dev/null +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text.Json.Serialization; +using Dalamud.Logging; + +namespace Pal.Client.Configuration +{ + public class AccountConfigurationV7 : IAccountConfiguration + { + [JsonConstructor] + public AccountConfigurationV7() + { + } + + public AccountConfigurationV7(string server, Guid accountId) + { + Server = server; + EncryptedId = EncryptAccountId(accountId); + } + + [Obsolete("for V1 import")] + public AccountConfigurationV7(string server, string accountId) + { + Server = server; + + if (accountId.StartsWith("s:")) + EncryptedId = accountId; + else if (Guid.TryParse(accountId, out Guid guid)) + EncryptedId = EncryptAccountId(guid); + else + throw new InvalidOperationException("invalid account id format"); + } + + public string EncryptedId { get; init; } = null!; + + public string Server { get; set; } = null!; + + [JsonIgnore] public bool IsUsable => DecryptAccountId(EncryptedId) != null; + + [JsonIgnore] public Guid AccountId => DecryptAccountId(EncryptedId) ?? throw new InvalidOperationException(); + + public List CachedRoles { get; set; } = new(); + + private Guid? DecryptAccountId(string id) + { + if (Guid.TryParse(id, out Guid guid) && guid != Guid.Empty) + return guid; + + if (!id.StartsWith("s:")) + throw new InvalidOperationException("invalid prefix"); + + try + { + byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(id.Substring(2)), + ConfigurationData.Entropy, DataProtectionScope.CurrentUser); + return new Guid(guidBytes); + } + catch (CryptographicException e) + { + PluginLog.Verbose(e, $"Could not load account id {id}"); + return null; + } + } + + private string EncryptAccountId(Guid g) + { + try + { + byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), ConfigurationData.Entropy, + DataProtectionScope.CurrentUser); + return $"s:{Convert.ToBase64String(guidBytes)}"; + } + catch (CryptographicException) + { + return g.ToString(); + } + } + } +} diff --git a/Pal.Client/Configuration/ConfigurationData.cs b/Pal.Client/Configuration/ConfigurationData.cs new file mode 100644 index 0000000..4131246 --- /dev/null +++ b/Pal.Client/Configuration/ConfigurationData.cs @@ -0,0 +1,7 @@ +namespace Pal.Client.Configuration +{ + internal static class ConfigurationData + { + internal static readonly byte[] Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 }; + } +} diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs new file mode 100644 index 0000000..09cfbab --- /dev/null +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using Dalamud.Logging; +using Dalamud.Plugin; +using ImGuiNET; +using Newtonsoft.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Pal.Client.Configuration +{ + internal class ConfigurationManager + { + private readonly DalamudPluginInterface _pluginInterface; + + public ConfigurationManager(DalamudPluginInterface pluginInterface) + { + _pluginInterface = pluginInterface; + } + + public string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); + +#pragma warning disable CS0612 +#pragma warning disable CS0618 + public void Migrate() + { + if (_pluginInterface.ConfigFile.Exists) + { + PluginLog.Information("Migrating config file from v1-v6 format"); + + ConfigurationV1 configurationV1 = + JsonConvert.DeserializeObject( + File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1(); + configurationV1.Migrate(); + configurationV1.Save(); + + var v7 = MigrateToV7(configurationV1); + Save(v7); + + File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); + } + } + + public IPalacePalConfiguration Load() + { + return JsonSerializer.Deserialize(File.ReadAllText(ConfigPath, Encoding.UTF8)) ?? + new ConfigurationV7(); + } + + public void Save(IConfigurationInConfigDirectory config) + { + File.WriteAllText(ConfigPath, + JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true }), + Encoding.UTF8); + } + + private ConfigurationV7 MigrateToV7(ConfigurationV1 v1) + { + ConfigurationV7 v7 = new() + { + Version = 7, + FirstUse = v1.FirstUse, + Mode = v1.Mode, + BetaKey = v1.BetaKey, + + DeepDungeons = new DeepDungeonConfiguration + { + Traps = new MarkerConfiguration + { + Show = v1.ShowTraps, + Color = ImGui.ColorConvertFloat4ToU32(v1.TrapColor), + Fill = false + }, + HoardCoffers = new MarkerConfiguration + { + Show = v1.ShowHoard, + Color = ImGui.ColorConvertFloat4ToU32(v1.HoardColor), + Fill = false + }, + SilverCoffers = new MarkerConfiguration + { + Show = v1.ShowSilverCoffers, + Color = ImGui.ColorConvertFloat4ToU32(v1.SilverCofferColor), + Fill = v1.FillSilverCoffers + } + } + }; + + foreach (var (server, oldAccount) in v1.Accounts) + { + string? accountId = oldAccount.Id; + if (string.IsNullOrEmpty(accountId)) + continue; + + IAccountConfiguration newAccount = v7.CreateAccount(server, 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.cs b/Pal.Client/Configuration/ConfigurationV1.cs similarity index 60% rename from Pal.Client/Configuration.cs rename to Pal.Client/Configuration/ConfigurationV1.cs index b1a88a7..ba47de8 100644 --- a/Pal.Client/Configuration.cs +++ b/Pal.Client/Configuration/ConfigurationV1.cs @@ -1,5 +1,4 @@ -using Dalamud.Configuration; -using Dalamud.Logging; +using Dalamud.Logging; using ECommons.Schedulers; using Newtonsoft.Json; using Pal.Client.Scheduled; @@ -9,15 +8,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using System.Security.Cryptography; using Pal.Client.Extensions; -namespace Pal.Client +namespace Pal.Client.Configuration { - public class Configuration : IPluginConfiguration + [Obsolete] + public class ConfigurationV1 { - private static readonly byte[] Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 }; - public int Version { get; set; } = 6; #region Saved configuration values @@ -55,7 +52,6 @@ namespace Pal.Client public string BetaKey { get; set; } = ""; #endregion -#pragma warning disable CS0612 // Type or member is obsolete public void Migrate() { if (Version == 1) @@ -78,7 +74,7 @@ namespace Pal.Client Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo { - Id = x.Value + Id = x.Value.ToString() // encryption happens in V7 migration at latest }); Version = 3; Save(); @@ -140,108 +136,23 @@ namespace Pal.Client Save(); } } -#pragma warning restore CS0612 // Type or member is obsolete public void Save() { - Service.PluginInterface.SavePluginConfig(this); + File.WriteAllText(Service.PluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings + { + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + TypeNameHandling = TypeNameHandling.Objects + })); Service.Plugin.EarlyEventQueue.Enqueue(new QueuedConfigUpdate()); } - public enum EMode - { - /// - /// Fetches trap locations from remote server. - /// - Online = 1, - - /// - /// Only shows traps found by yourself uisng a pomander of sight. - /// - Offline = 2, - } - - public enum ERenderer - { - /// - Simple = 0, - - /// - Splatoon = 1, - } - public class AccountInfo { - [JsonConverter(typeof(AccountIdConverter))] - public Guid? Id { get; set; } - - /// - /// 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. - /// + public string? Id { get; set; } public List CachedRoles { get; set; } = new(); } - public class AccountIdConverter : JsonConverter - { - public override bool CanConvert(Type objectType) => true; - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.String) - { - string? text = reader.Value?.ToString(); - if (string.IsNullOrEmpty(text)) - return null; - - if (Guid.TryParse(text, out Guid guid) && guid != Guid.Empty) - return guid; - - if (text.StartsWith("s:")) - { - try - { - byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(text.Substring(2)), Entropy, DataProtectionScope.CurrentUser); - return new Guid(guidBytes); - } - catch (CryptographicException e) - { - PluginLog.Error(e, "Could not load account id"); - return null; - } - } - } - throw new JsonSerializationException(); - } - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - Guid g = (Guid)value; - string text; - try - { - byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), Entropy, DataProtectionScope.CurrentUser); - text = $"s:{Convert.ToBase64String(guidBytes)}"; - } - catch (CryptographicException) - { - text = g.ToString(); - } - - writer.WriteValue(text); - } - } - public class ImportHistoryEntry { public Guid Id { get; set; } diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs new file mode 100644 index 0000000..0d2aa52 --- /dev/null +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Pal.Client.Configuration; + +public 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(); + + [JsonIgnore] + [Obsolete] + public List ImportHistory { get; set; } = new(); + + public IAccountConfiguration CreateAccount(string server, Guid accountId) + { + var account = new AccountConfigurationV7(server, accountId); + Accounts.Add(account); + return account; + } + + [Obsolete("for V1 import")] + internal IAccountConfiguration CreateAccount(string server, string accountId) + { + var account = new AccountConfigurationV7(server, accountId); + Accounts.Add(account); + return account; + } + + 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); + } +} diff --git a/Pal.Client/Configuration/EMode.cs b/Pal.Client/Configuration/EMode.cs new file mode 100644 index 0000000..40d17d7 --- /dev/null +++ b/Pal.Client/Configuration/EMode.cs @@ -0,0 +1,15 @@ +namespace Pal.Client.Configuration +{ + public enum EMode + { + /// + /// Fetches trap locations from remote server. + /// + Online = 1, + + /// + /// Only shows traps found by yourself uisng a pomander of sight. + /// + Offline = 2, + } +} diff --git a/Pal.Client/Configuration/ERenderer.cs b/Pal.Client/Configuration/ERenderer.cs new file mode 100644 index 0000000..2ea7568 --- /dev/null +++ b/Pal.Client/Configuration/ERenderer.cs @@ -0,0 +1,11 @@ +namespace Pal.Client.Configuration +{ + public enum ERenderer + { + /// + Simple = 0, + + /// + Splatoon = 1, + } +} diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs new file mode 100644 index 0000000..2f608a7 --- /dev/null +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; + +namespace Pal.Client.Configuration +{ + 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; } + + DeepDungeonConfiguration DeepDungeons { get; set; } + RendererConfiguration Renderer { get; set; } + + [Obsolete] + List ImportHistory { get; } + + IAccountConfiguration CreateAccount(string server, Guid accountId); + IAccountConfiguration? FindAccount(string server); + void RemoveAccount(string server); + } + + 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 class MarkerConfiguration + { + public bool Show { get; set; } + 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 + { + public bool IsUsable { get; } + public string Server { get; } + public 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. + /// + public List CachedRoles { get; set; } + } +} diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index 9b3744f..d72636a 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Pal.Client.Extensions; using Pal.Client.Properties; +using Pal.Client.Configuration; namespace Pal.Client.Net { @@ -18,7 +19,7 @@ namespace Pal.Client.Net { private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) { - if (Service.Configuration.Mode != Configuration.EMode.Online) + if (Service.Configuration.Mode != EMode.Online) { PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection"); return (false, Localization.ConnectionError_NotOnline); @@ -46,19 +47,20 @@ namespace Pal.Client.Net cancellationToken.ThrowIfCancellationRequested(); var accountClient = new AccountService.AccountServiceClient(_channel); - if (AccountId == null) + IAccountConfiguration? configuredAccount = Service.Configuration.FindAccount(RemoteUrl); + if (configuredAccount == null) { PluginLog.Information($"TryConnect: No account information saved for {RemoteUrl}, creating new account"); var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); if (createAccountReply.Success) { - Account = new Configuration.AccountInfo - { - Id = Guid.Parse(createAccountReply.AccountId), - }; - PluginLog.Information($"TryConnect: Account created with id {FormattedPartialAccountId}"); + if (Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) + throw new InvalidOperationException("invalid account id returned"); - Service.Configuration.Save(); + configuredAccount = Service.Configuration.CreateAccount(RemoteUrl, accountId); + PluginLog.Information($"TryConnect: Account created with id {accountId.ToPartialId()}"); + + Service.ConfigurationManager.Save(Service.Configuration); } else { @@ -74,27 +76,24 @@ namespace Pal.Client.Net cancellationToken.ThrowIfCancellationRequested(); - if (AccountId == null) + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (configuredAccount == null) { - PluginLog.Warning("TryConnect: No account id to login with"); + PluginLog.Warning("TryConnect: No account to login with"); return (false, Localization.ConnectionError_CreateAccountReturnedNoId); } if (!_loginInfo.IsValid) { - PluginLog.Information($"TryConnect: Logging in with account id {FormattedPartialAccountId}"); - LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = AccountId?.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + PluginLog.Information($"TryConnect: Logging in with account id {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) { - PluginLog.Information($"TryConnect: Login successful with account id: {FormattedPartialAccountId}"); + PluginLog.Information($"TryConnect: Login successful with account id: {configuredAccount.AccountId.ToPartialId()}"); _loginInfo = new LoginInfo(loginReply.AuthToken); - var account = Account; - if (account != null) - { - account.CachedRoles = _loginInfo.Claims?.Roles.ToList() ?? new List(); - Service.Configuration.Save(); - } + configuredAccount.CachedRoles = _loginInfo.Claims?.Roles.ToList() ?? new List(); + Service.ConfigurationManager.Save(Service.Configuration); } else { @@ -102,8 +101,8 @@ namespace Pal.Client.Net _loginInfo = new LoginInfo(null); if (loginReply.Error == LoginError.InvalidAccountId) { - Account = null; - Service.Configuration.Save(); + Service.Configuration.RemoveAccount(RemoteUrl); + Service.ConfigurationManager.Save(Service.Configuration); if (retry) { PluginLog.Information("TryConnect: Attempting connection retry without account id"); diff --git a/Pal.Client/Net/RemoteApi.Utils.cs b/Pal.Client/Net/RemoteApi.Utils.cs index 7dff878..0ff7af8 100644 --- a/Pal.Client/Net/RemoteApi.Utils.cs +++ b/Pal.Client/Net/RemoteApi.Utils.cs @@ -59,7 +59,7 @@ namespace Pal.Client.Net if (Service.Configuration.Mode != Configuration.EMode.Online) return false; - var account = Account; + var account = Service.Configuration.FindAccount(RemoteUrl); return account == null || account.CachedRoles.Contains(role); } } diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index a25afbf..f41e7f9 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -2,16 +2,19 @@ using Grpc.Net.Client; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; +using System.Linq; using Pal.Client.Extensions; +using Pal.Client.Configuration; namespace Pal.Client.Net { internal partial class RemoteApi : IDisposable { #if DEBUG - public static string RemoteUrl { get; } = "http://localhost:5145"; + public const string RemoteUrl = "http://localhost:5145"; #else - public static string RemoteUrl { get; } = "https://pal.μ.tv"; + public const string RemoteUrl = "https://pal.μ.tv"; #endif private readonly string _userAgent = $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; @@ -21,24 +24,6 @@ namespace Pal.Client.Net private LoginInfo _loginInfo = new(null); private bool _warnedAboutUpgrade; - public Configuration.AccountInfo? Account - { - get => Service.Configuration.Accounts.TryGetValue(RemoteUrl, out Configuration.AccountInfo? accountInfo) ? accountInfo : null; - set - { - if (value != null) - Service.Configuration.Accounts[RemoteUrl] = value; - else - Service.Configuration.Accounts.Remove(RemoteUrl); - } - } - - public Guid? AccountId => Account?.Id; - - public string? PartialAccountId => Account?.Id?.ToPartialId(); - - private string FormattedPartialAccountId => PartialAccountId ?? "[no account id]"; - public void Dispose() { PluginLog.Debug("Disposing gRPC channel"); diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 4e5e893..1e486ac 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -27,6 +27,9 @@ using System.Threading.Tasks; using Pal.Client.Extensions; using Pal.Client.Properties; using ECommons; +using ECommons.Schedulers; +using Pal.Client.Configuration; +using Pal.Client.Net; namespace Pal.Client { @@ -71,8 +74,10 @@ namespace Pal.Client pluginInterface.Create(); Service.Plugin = this; - Service.Configuration = (Configuration?)pluginInterface.GetPluginConfig() ?? pluginInterface.Create()!; - Service.Configuration.Migrate(); + + Service.ConfigurationManager = new(pluginInterface); + Service.ConfigurationManager.Migrate(); + Service.Configuration = Service.ConfigurationManager.Load(); ResetRenderer(); @@ -146,7 +151,7 @@ namespace Pal.Client return; configWindow.IsOpen = true; - configWindow.TestConnection(); + var _ = new TickScheduler(() => configWindow.TestConnection()); break; #if DEBUG @@ -196,6 +201,7 @@ namespace Pal.Client Service.Framework.Update -= OnFrameworkUpdate; Service.Chat.ChatMessage -= OnChatMessage; + Service.WindowSystem.GetWindow()?.Dispose(); Service.WindowSystem.RemoveAllWindows(); Service.RemoteApi.Dispose(); @@ -318,7 +324,7 @@ namespace Pal.Client var currentFloorMarkers = currentFloor.Markers; bool updateSeenMarkers = false; - var partialAccountId = Service.RemoteApi.PartialAccountId; + var partialAccountId = Service.Configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); foreach (var visibleMarker in visibleMarkers) { Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); @@ -343,7 +349,7 @@ namespace Pal.Client saveMarkers = true; } - if (!recreateLayout && currentFloorMarkers.Count > 0 && (config.OnlyVisibleTrapsAfterPomander || config.OnlyVisibleHoardAfterPomander)) + if (!recreateLayout && currentFloorMarkers.Count > 0 && (config.DeepDungeons.Traps.OnlyVisibleAfterPomander || config.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) { try @@ -399,15 +405,15 @@ namespace Pal.Client List elements = new(); foreach (var marker in currentFloorMarkers) { - if (marker.Seen || config.Mode == Configuration.EMode.Online || marker is { WasImported: true, Imports.Count: > 0 }) + if (marker.Seen || config.Mode == EMode.Online || marker is { WasImported: true, Imports.Count: > 0 }) { - if (marker.Type == Marker.EType.Trap && config.ShowTraps) + if (marker.Type == Marker.EType.Trap) { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers)); + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.Traps); } - else if (marker.Type == Marker.EType.Hoard && config.ShowHoard) + else if (marker.Type == Marker.EType.Hoard) { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers)); + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.HoardCoffers); } } } @@ -436,9 +442,9 @@ namespace Pal.Client { EphemeralMarkers.Add(marker); - if (marker.Type == Marker.EType.SilverCoffer && config.ShowSilverCoffers) + if (marker.Type == Marker.EType.SilverCoffer && config.DeepDungeons.SilverCoffers.Show) { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.FillSilverCoffers); + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.SilverCoffers); } } @@ -453,12 +459,12 @@ namespace Pal.Client { switch (marker.Type) { - case Marker.EType.Trap when PomanderOfSight == PomanderState.Inactive || !Service.Configuration.OnlyVisibleTrapsAfterPomander || visibleMarkers.Any(x => x == marker): - return ImGui.ColorConvertFloat4ToU32(Service.Configuration.TrapColor); - case Marker.EType.Hoard when PomanderOfIntuition == PomanderState.Inactive || !Service.Configuration.OnlyVisibleHoardAfterPomander || visibleMarkers.Any(x => x == marker): - return ImGui.ColorConvertFloat4ToU32(Service.Configuration.HoardColor); + case Marker.EType.Trap when PomanderOfSight == PomanderState.Inactive || !Service.Configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + return Service.Configuration.DeepDungeons.Traps.Color; + case Marker.EType.Hoard when PomanderOfIntuition == PomanderState.Inactive || !Service.Configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + return Service.Configuration.DeepDungeons.HoardCoffers.Color; case Marker.EType.SilverCoffer: - return ImGui.ColorConvertFloat4ToU32(Service.Configuration.SilverCofferColor); + return Service.Configuration.DeepDungeons.SilverCoffers.Color; case Marker.EType.Trap: case Marker.EType.Hoard: return ColorInvisible; @@ -467,9 +473,12 @@ namespace Pal.Client } } - private void CreateRenderElement(Marker marker, List elements, uint color, bool fill = false) + private void CreateRenderElement(Marker marker, List elements, uint color, MarkerConfiguration config) { - var element = Renderer.CreateElement(marker.Type, marker.Position, color, fill); + if (!config.Show) + return; + + var element = Renderer.CreateElement(marker.Type, marker.Position, color, config.Fill); marker.RenderElement = element; elements.Add(element); } @@ -651,15 +660,15 @@ namespace Pal.Client internal void ResetRenderer() { - if (Renderer is SplatoonRenderer && Service.Configuration.Renderer == Configuration.ERenderer.Splatoon) + if (Renderer is SplatoonRenderer && Service.Configuration.Renderer.SelectedRenderer == ERenderer.Splatoon) return; - else if (Renderer is SimpleRenderer && Service.Configuration.Renderer == Configuration.ERenderer.Simple) + else if (Renderer is SimpleRenderer && Service.Configuration.Renderer.SelectedRenderer == ERenderer.Simple) return; if (Renderer is IDisposable disposable) disposable.Dispose(); - if (Service.Configuration.Renderer == Configuration.ERenderer.Splatoon) + if (Service.Configuration.Renderer.SelectedRenderer == ERenderer.Splatoon) Renderer = new SplatoonRenderer(Service.PluginInterface, this); else Renderer = new SimpleRenderer(); diff --git a/Pal.Client/Rendering/SimpleRenderer.cs b/Pal.Client/Rendering/SimpleRenderer.cs index c2c047b..58ed62c 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -120,7 +120,7 @@ namespace Pal.Client.Rendering { case Marker.EType.Hoard: // ignore distance if this is a found hoard coffer - if (Service.Plugin.PomanderOfIntuition == Plugin.PomanderState.Active && Service.Configuration.OnlyVisibleHoardAfterPomander) + if (Service.Plugin.PomanderOfIntuition == Plugin.PomanderState.Active && Service.Configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) break; goto case Marker.EType.Trap; diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 890de80..45623d8 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Numerics; using Pal.Client.Extensions; using Pal.Client.Properties; +using Pal.Client.Configuration; namespace Pal.Client.Scheduled { @@ -46,14 +47,14 @@ namespace Pal.Client.Scheduled } config.ImportHistory.RemoveAll(hist => oldExportIds.Contains(hist.Id) || hist.Id == _exportId); - config.ImportHistory.Add(new Configuration.ImportHistoryEntry + config.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry { Id = _exportId, RemoteUrl = _export.ServerUrl, ExportedAt = _export.CreatedAt.ToDateTime(), ImportedAt = DateTime.UtcNow, }); - config.Save(); + Service.ConfigurationManager.Save(config); recreateLayout = true; saveMarkers = true; diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index 871ed47..44a88a6 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Pal.Client.Extensions; +using Pal.Client.Net; using static Pal.Client.Plugin; namespace Pal.Client.Scheduled @@ -45,7 +47,7 @@ namespace Pal.Client.Scheduled break; case SyncType.MarkSeen: - var partialAccountId = Service.RemoteApi.PartialAccountId; + var partialAccountId = Service.Configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); if (partialAccountId == null) break; foreach (var remoteMarker in remoteMarkers) diff --git a/Pal.Client/Service.cs b/Pal.Client/Service.cs index 0b0d90d..4b54075 100644 --- a/Pal.Client/Service.cs +++ b/Pal.Client/Service.cs @@ -8,6 +8,7 @@ using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; +using Pal.Client.Configuration; using Pal.Client.Net; namespace Pal.Client @@ -27,7 +28,8 @@ namespace Pal.Client internal static Plugin Plugin { get; set; } = null!; internal static WindowSystem WindowSystem { get; } = new(typeof(Service).AssemblyQualifiedName); internal static RemoteApi RemoteApi { get; } = new(); - internal static Configuration Configuration { get; set; } = null!; + internal static ConfigurationManager ConfigurationManager { get; set; } = null!; + internal static IPalacePalConfiguration Configuration { get; set; } = null!; internal static Hooks Hooks { get; set; } = null!; } } diff --git a/Pal.Client/Windows/AgreementWindow.cs b/Pal.Client/Windows/AgreementWindow.cs index 3cd3a29..41fb25c 100644 --- a/Pal.Client/Windows/AgreementWindow.cs +++ b/Pal.Client/Windows/AgreementWindow.cs @@ -69,7 +69,7 @@ namespace Pal.Client.Windows { config.Mode = (Configuration.EMode)_choice; config.FirstUse = false; - config.Save(); + Service.ConfigurationManager.Save(config); IsOpen = false; } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 7787296..535f67e 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -19,23 +19,18 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Pal.Client.Properties; +using Pal.Client.Configuration; namespace Pal.Client.Windows { - internal class ConfigWindow : Window, ILanguageChanged + internal class ConfigWindow : Window, ILanguageChanged, IDisposable { private const string WindowId = "###PalPalaceConfig"; private int _mode; private int _renderer; - private bool _showTraps; - private Vector4 _trapColor; - private bool _onlyVisibleTrapsAfterPomander; - private bool _showHoard; - private Vector4 _hoardColor; - private bool _onlyVisibleHoardAfterPomander; - private bool _showSilverCoffers; - private Vector4 _silverCofferColor; - private bool _fillSilverCoffers; + private ConfigurableMarker _trapConfig = new(); + private ConfigurableMarker _hoardConfig = new(); + private ConfigurableMarker _silverConfig = new(); private string? _connectionText; private bool _switchToCommunityTab; @@ -67,20 +62,19 @@ namespace Pal.Client.Windows WindowName = $"{Localization.Palace_Pal} v{version}{WindowId}"; } + public void Dispose() + { + _testConnectionCts?.Cancel(); + } + public override void OnOpen() { var config = Service.Configuration; _mode = (int)config.Mode; - _renderer = (int)config.Renderer; - _showTraps = config.ShowTraps; - _trapColor = config.TrapColor; - _onlyVisibleTrapsAfterPomander = config.OnlyVisibleTrapsAfterPomander; - _showHoard = config.ShowHoard; - _hoardColor = config.HoardColor; - _onlyVisibleHoardAfterPomander = config.OnlyVisibleHoardAfterPomander; - _showSilverCoffers = config.ShowSilverCoffers; - _silverCofferColor = config.SilverCofferColor; - _fillSilverCoffers = config.FillSilverCoffers; + _renderer = (int)config.Renderer.SelectedRenderer; + _trapConfig = new ConfigurableMarker(config.DeepDungeons.Traps); + _hoardConfig = new ConfigurableMarker(config.DeepDungeons.HoardCoffers); + _silverConfig = new ConfigurableMarker(config.DeepDungeons.SilverCoffers); _connectionText = null; } @@ -113,18 +107,13 @@ namespace Pal.Client.Windows if (save || saveAndClose) { var config = Service.Configuration; - config.Mode = (Configuration.EMode)_mode; - config.Renderer = (Configuration.ERenderer)_renderer; - config.ShowTraps = _showTraps; - config.TrapColor = _trapColor; - config.OnlyVisibleTrapsAfterPomander = _onlyVisibleTrapsAfterPomander; - config.ShowHoard = _showHoard; - config.HoardColor = _hoardColor; - config.OnlyVisibleHoardAfterPomander = _onlyVisibleHoardAfterPomander; - config.ShowSilverCoffers = _showSilverCoffers; - config.SilverCofferColor = _silverCofferColor; - config.FillSilverCoffers = _fillSilverCoffers; - config.Save(); + config.Mode = (EMode)_mode; + config.Renderer.SelectedRenderer = (ERenderer)_renderer; + config.DeepDungeons.Traps = _trapConfig.Build(); + config.DeepDungeons.HoardCoffers = _hoardConfig.Build(); + config.DeepDungeons.SilverCoffers = _silverConfig.Build(); + + Service.ConfigurationManager.Save(config); if (saveAndClose) IsOpen = false; @@ -135,12 +124,12 @@ namespace Pal.Client.Windows { if (ImGui.BeginTabItem($"{Localization.ConfigTab_DeepDungeons}###TabDeepDungeons")) { - ImGui.Checkbox(Localization.Config_Traps_Show, ref _showTraps); + ImGui.Checkbox(Localization.Config_Traps_Show, ref _trapConfig.Show); ImGui.Indent(); - ImGui.BeginDisabled(!_showTraps); + ImGui.BeginDisabled(!_trapConfig.Show); ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_Traps_Color, ref _trapColor, ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_Traps_HideImpossible, ref _onlyVisibleTrapsAfterPomander); + 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(); @@ -148,12 +137,12 @@ namespace Pal.Client.Windows ImGui.Separator(); - ImGui.Checkbox(Localization.Config_HoardCoffers_Show, ref _showHoard); + ImGui.Checkbox(Localization.Config_HoardCoffers_Show, ref _hoardConfig.Show); ImGui.Indent(); - ImGui.BeginDisabled(!_showHoard); + ImGui.BeginDisabled(!_hoardConfig.Show); ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_HoardCoffers_Color, ref _hoardColor, ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_HoardCoffers_HideImpossible, ref _onlyVisibleHoardAfterPomander); + 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(); @@ -161,13 +150,13 @@ namespace Pal.Client.Windows ImGui.Separator(); - ImGui.Checkbox(Localization.Config_SilverCoffer_Show, ref _showSilverCoffers); + ImGui.Checkbox(Localization.Config_SilverCoffer_Show, ref _silverConfig.Show); ImGuiComponents.HelpMarker(Localization.Config_SilverCoffers_ToolTip); ImGui.Indent(); - ImGui.BeginDisabled(!_showSilverCoffers); + ImGui.BeginDisabled(!_silverConfig.Show); ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverCofferColor, ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_SilverCoffer_Filled, ref _fillSilverCoffers); + ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverConfig.Color, ImGuiColorEditFlags.NoInputs); + ImGui.Checkbox(Localization.Config_SilverCoffer_Filled, ref _silverConfig.Fill); ImGui.EndDisabled(); ImGui.Unindent(); @@ -190,13 +179,13 @@ namespace Pal.Client.Windows ImGui.TextWrapped(Localization.Explanation_3); ImGui.TextWrapped(Localization.Explanation_4); - ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _mode, (int)Configuration.EMode.Online); - ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, (int)Configuration.EMode.Offline); + ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _mode, (int)EMode.Online); + ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, (int)EMode.Offline); saveAndClose = ImGui.Button(Localization.SaveAndClose); ImGui.Separator(); - ImGui.BeginDisabled(Service.Configuration.Mode != Configuration.EMode.Online); + ImGui.BeginDisabled(Service.Configuration.Mode != EMode.Online); if (ImGui.Button(Localization.Config_TestConnection)) TestConnection(); @@ -294,8 +283,8 @@ namespace Pal.Client.Windows 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)Configuration.ERenderer.Splatoon); - ImGui.RadioButton($"{Localization.Config_Renderer_Simple} ({Localization.Config_Renderer_Simple_Hint})", ref _renderer, (int)Configuration.ERenderer.Simple); + 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(); @@ -307,7 +296,7 @@ namespace Pal.Client.Windows ImGui.Text(Localization.Config_Splatoon_Test); ImGui.BeginDisabled(!(Service.Plugin.Renderer is IDrawDebugItems)); if (ImGui.Button(Localization.Config_Splatoon_DrawCircles)) - (Service.Plugin.Renderer as IDrawDebugItems)?.DrawDebugItems(_trapColor, _hoardColor); + (Service.Plugin.Renderer as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, _hoardConfig.Color); ImGui.EndDisabled(); ImGui.EndTabItem(); @@ -328,17 +317,17 @@ namespace Pal.Client.Windows ImGui.Indent(); if (plugin.FloorMarkers.TryGetValue(plugin.LastTerritory, out var currentFloor)) { - if (_showTraps) + if (_trapConfig.Show) { int traps = currentFloor.Markers.Count(x => x.Type == Marker.EType.Trap); ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); } - if (_showHoard) + if (_hoardConfig.Show) { int hoardCoffers = currentFloor.Markers.Count(x => x.Type == Marker.EType.Hoard); ImGui.Text($"{hoardCoffers} known hoard coffer{(hoardCoffers == 1 ? "" : "s")}"); } - if (_showSilverCoffers) + if (_silverConfig.Show) { int silverCoffers = plugin.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); ImGui.Text($"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); @@ -440,5 +429,36 @@ namespace Pal.Client.Windows } }); } + + private 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 + }; + } + } } } From 4f8deea8e0dcb5672d5514d251c4444136ae0f0a Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 10:20:25 +0100 Subject: [PATCH 02/38] Config: account tweaks, UTF-8 fix, update server url --- .../Configuration/AccountConfigurationV7.cs | 21 +++++++++++++++---- .../Configuration/ConfigurationManager.cs | 14 ++++++------- .../Configuration/IPalacePalConfiguration.cs | 10 +++++---- Pal.Client/Net/RemoteApi.AccountService.cs | 15 ++++++++++--- Pal.Client/Net/RemoteApi.cs | 2 +- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index beb8695..dd994ca 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -32,9 +32,11 @@ namespace Pal.Client.Configuration throw new InvalidOperationException("invalid account id format"); } - public string EncryptedId { get; init; } = null!; + [JsonPropertyName("Id")] + [JsonInclude] + public string EncryptedId { get; private set; } = null!; - public string Server { get; set; } = null!; + public string Server { get; init; } = null!; [JsonIgnore] public bool IsUsable => DecryptAccountId(EncryptedId) != null; @@ -56,7 +58,7 @@ namespace Pal.Client.Configuration ConfigurationData.Entropy, DataProtectionScope.CurrentUser); return new Guid(guidBytes); } - catch (CryptographicException e) + catch (Exception e) { PluginLog.Verbose(e, $"Could not load account id {id}"); return null; @@ -71,10 +73,21 @@ namespace Pal.Client.Configuration DataProtectionScope.CurrentUser); return $"s:{Convert.ToBase64String(guidBytes)}"; } - catch (CryptographicException) + catch (Exception) { return g.ToString(); } } + + public bool EncryptIfNeeded() + { + if (Guid.TryParse(EncryptedId, out Guid g)) + { + string oldId = EncryptedId; + EncryptedId = EncryptAccountId(g); + return oldId != EncryptedId; + } + return false; + } } } diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 09cfbab..9aabd3c 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -1,13 +1,12 @@ -using System; -using System.IO; +using System.IO; using System.Linq; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using Dalamud.Logging; using Dalamud.Plugin; using ImGuiNET; -using Newtonsoft.Json; -using JsonSerializer = System.Text.Json.JsonSerializer; +using NJson = Newtonsoft.Json; namespace Pal.Client.Configuration { @@ -31,7 +30,7 @@ namespace Pal.Client.Configuration PluginLog.Information("Migrating config file from v1-v6 format"); ConfigurationV1 configurationV1 = - JsonConvert.DeserializeObject( + NJson.JsonConvert.DeserializeObject( File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1(); configurationV1.Migrate(); configurationV1.Save(); @@ -52,7 +51,7 @@ namespace Pal.Client.Configuration public void Save(IConfigurationInConfigDirectory config) { File.WriteAllText(ConfigPath, - JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true }), + JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), Encoding.UTF8); } @@ -94,7 +93,8 @@ namespace Pal.Client.Configuration if (string.IsNullOrEmpty(accountId)) continue; - IAccountConfiguration newAccount = v7.CreateAccount(server, accountId); + string serverName = server.Replace(".μ.tv", ".liza.sh"); + IAccountConfiguration newAccount = v7.CreateAccount(serverName, accountId); newAccount.CachedRoles = oldAccount.CachedRoles.ToList(); } diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index 2f608a7..ecfb8a2 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -72,9 +72,9 @@ namespace Pal.Client.Configuration public interface IAccountConfiguration { - public bool IsUsable { get; } - public string Server { get; } - public Guid AccountId { get; } + bool IsUsable { get; } + string Server { get; } + Guid AccountId { get; } /// /// This is taken from the JWT, and is only refreshed on a successful login. @@ -84,6 +84,8 @@ namespace Pal.Client.Configuration /// 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. /// - public List CachedRoles { get; set; } + List CachedRoles { get; set; } + + bool EncryptIfNeeded(); } } diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index d72636a..96dc32e 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -54,7 +54,7 @@ namespace Pal.Client.Net 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)) + if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) throw new InvalidOperationException("invalid account id returned"); configuredAccount = Service.Configuration.CreateAccount(RemoteUrl, accountId); @@ -92,8 +92,17 @@ namespace Pal.Client.Net PluginLog.Information($"TryConnect: Login successful with account id: {configuredAccount.AccountId.ToPartialId()}"); _loginInfo = new LoginInfo(loginReply.AuthToken); - configuredAccount.CachedRoles = _loginInfo.Claims?.Roles.ToList() ?? new List(); - Service.ConfigurationManager.Save(Service.Configuration); + bool save = configuredAccount.EncryptIfNeeded(); + + List newRoles = _loginInfo.Claims?.Roles.ToList() ?? new(); + if (!newRoles.SequenceEqual(configuredAccount.CachedRoles)) + { + configuredAccount.CachedRoles = newRoles; + save = true; + } + + if (save) + Service.ConfigurationManager.Save(Service.Configuration); } else { diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index f41e7f9..1f7d6d9 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -14,7 +14,7 @@ namespace Pal.Client.Net #if DEBUG public const string RemoteUrl = "http://localhost:5145"; #else - public const string RemoteUrl = "https://pal.μ.tv"; + 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)}"; From 16a17e0dcfd95a2b3de458d1909af6b23d26d31b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 10:46:04 +0100 Subject: [PATCH 03/38] Config: Add EF Core --- Pal.Client/Pal.Client.csproj | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index eda3e88..96ae04d 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -5,6 +5,7 @@ 11.0 2.15 enable + win-x64 @@ -42,8 +43,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + From 550fa92a535c48833fc5d2ce5ae748f56af40040 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 13:00:00 +0100 Subject: [PATCH 04/38] Config: Improve account model --- .../Configuration/AccountConfigurationV7.cs | 105 ++++++++++++------ Pal.Client/Configuration/ConfigurationData.cs | 7 +- .../Configuration/ConfigurationManager.cs | 26 ++--- Pal.Client/Pal.Client.csproj | 1 + 4 files changed, 93 insertions(+), 46 deletions(-) diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index dd994ca..5082463 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography; using System.Text.Json.Serialization; using Dalamud.Logging; @@ -16,7 +17,7 @@ namespace Pal.Client.Configuration public AccountConfigurationV7(string server, Guid accountId) { Server = server; - EncryptedId = EncryptAccountId(accountId); + (EncryptedId, Entropy, Format) = EncryptAccountId(accountId); } [Obsolete("for V1 import")] @@ -25,69 +26,111 @@ namespace Pal.Client.Configuration Server = server; if (accountId.StartsWith("s:")) - EncryptedId = accountId; + { + EncryptedId = accountId.Substring(2); + Entropy = ConfigurationData.FixedV1Entropy; + Format = EFormat.UseProtectedData; + + // try to migrate away from v1 entropy if possible + Guid? decryptedId = DecryptAccountId(); + if (decryptedId != null) + (EncryptedId, Entropy, Format) = EncryptAccountId(decryptedId.Value); + } else if (Guid.TryParse(accountId, out Guid guid)) - EncryptedId = EncryptAccountId(guid); + (EncryptedId, Entropy, Format) = EncryptAccountId(guid); else - throw new InvalidOperationException("invalid account id format"); + throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}"); } + [JsonInclude] + 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] public string EncryptedId { get; private set; } = null!; + [JsonInclude] + public byte[]? Entropy { get; private set; } + public string Server { get; init; } = null!; - [JsonIgnore] public bool IsUsable => DecryptAccountId(EncryptedId) != null; + [JsonIgnore] public bool IsUsable => DecryptAccountId() != null; - [JsonIgnore] public Guid AccountId => DecryptAccountId(EncryptedId) ?? throw new InvalidOperationException(); + [JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read"); public List CachedRoles { get; set; } = new(); - private Guid? DecryptAccountId(string id) + private Guid? DecryptAccountId() { - if (Guid.TryParse(id, out Guid guid) && guid != Guid.Empty) - return guid; - - if (!id.StartsWith("s:")) - throw new InvalidOperationException("invalid prefix"); - - try + if (Format == EFormat.UseProtectedData) { - byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(id.Substring(2)), - ConfigurationData.Entropy, DataProtectionScope.CurrentUser); - return new Guid(guidBytes); + try + { + byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser); + return new Guid(guidBytes); + } + catch (Exception e) + { + PluginLog.Verbose(e, $"Could not load account id {EncryptedId}"); + return null; + } } - catch (Exception e) - { - PluginLog.Verbose(e, $"Could not load account id {id}"); + else if (Format == EFormat.Unencrypted) + return Guid.Parse(EncryptedId); + else return null; - } } - private string EncryptAccountId(Guid g) + private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g) { try { - byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), ConfigurationData.Entropy, - DataProtectionScope.CurrentUser); - return $"s:{Convert.ToBase64String(guidBytes)}"; + byte[] entropy = RandomNumberGenerator.GetBytes(16); + byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser); + return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData); } catch (Exception) { - return g.ToString(); + return (g.ToString(), null, EFormat.Unencrypted); } } - + public bool EncryptIfNeeded() { - if (Guid.TryParse(EncryptedId, out Guid g)) + if (Format == EFormat.Unencrypted) { - string oldId = EncryptedId; - EncryptedId = EncryptAccountId(g); - return oldId != EncryptedId; + var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId)); + if (newFormat != EFormat.Unencrypted) + { + EncryptedId = newId; + Entropy = newEntropy; + Format = newFormat; + return true; + } } + +#pragma warning disable CS0618 // Type or member is obsolete + if (Format == EFormat.UseProtectedData && ConfigurationData.FixedV1Entropy.SequenceEqual(Entropy ?? Array.Empty())) + { + Guid? g = DecryptAccountId(); + if (g != null) + { + (EncryptedId, Entropy, Format) = EncryptAccountId(g.Value); + return true; + } + } +#pragma warning restore CS0618 // Type or member is obsolete + return false; } + + public enum EFormat + { + Unencrypted = 1, + UseProtectedData = 2, + } } } diff --git a/Pal.Client/Configuration/ConfigurationData.cs b/Pal.Client/Configuration/ConfigurationData.cs index 4131246..e8cb169 100644 --- a/Pal.Client/Configuration/ConfigurationData.cs +++ b/Pal.Client/Configuration/ConfigurationData.cs @@ -1,7 +1,10 @@ -namespace Pal.Client.Configuration +using System; + +namespace Pal.Client.Configuration { internal static class ConfigurationData { - internal static readonly byte[] Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 }; + [Obsolete("for V1 import")] + internal static readonly byte[] FixedV1Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 }; } } diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 9aabd3c..86757a7 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -21,6 +21,19 @@ namespace Pal.Client.Configuration public string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); + public IPalacePalConfiguration Load() + { + return JsonSerializer.Deserialize(File.ReadAllText(ConfigPath, Encoding.UTF8)) ?? + new ConfigurationV7(); + } + + public void Save(IConfigurationInConfigDirectory config) + { + File.WriteAllText(ConfigPath, + JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), + Encoding.UTF8); + } + #pragma warning disable CS0612 #pragma warning disable CS0618 public void Migrate() @@ -42,19 +55,6 @@ namespace Pal.Client.Configuration } } - public IPalacePalConfiguration Load() - { - return JsonSerializer.Deserialize(File.ReadAllText(ConfigPath, Encoding.UTF8)) ?? - new ConfigurationV7(); - } - - public void Save(IConfigurationInConfigDirectory config) - { - File.WriteAllText(ConfigPath, - JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), - Encoding.UTF8); - } - private ConfigurationV7 MigrateToV7(ConfigurationV1 v1) { ConfigurationV7 v7 = new() diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index 96ae04d..fd6869a 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -14,6 +14,7 @@ Palace Pal true false + false true From d1cb7e08f2d364d22195f30ab462ce5054577955 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 13:27:41 +0100 Subject: [PATCH 05/38] Config: Make DPAPI support optional --- .../Configuration/AccountConfigurationV7.cs | 46 +++++++++++-------- Pal.Client/Configuration/ConfigurationData.cs | 31 ++++++++++++- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index 5082463..de28185 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -9,6 +9,8 @@ namespace Pal.Client.Configuration { public class AccountConfigurationV7 : IAccountConfiguration { + private const int EntropyLength = 16; + [JsonConstructor] public AccountConfigurationV7() { @@ -30,11 +32,7 @@ namespace Pal.Client.Configuration EncryptedId = accountId.Substring(2); Entropy = ConfigurationData.FixedV1Entropy; Format = EFormat.UseProtectedData; - - // try to migrate away from v1 entropy if possible - Guid? decryptedId = DecryptAccountId(); - if (decryptedId != null) - (EncryptedId, Entropy, Format) = EncryptAccountId(decryptedId.Value); + EncryptIfNeeded(); } else if (Guid.TryParse(accountId, out Guid guid)) (EncryptedId, Entropy, Format) = EncryptAccountId(guid); @@ -65,7 +63,7 @@ namespace Pal.Client.Configuration private Guid? DecryptAccountId() { - if (Format == EFormat.UseProtectedData) + if (Format == EFormat.UseProtectedData && ConfigurationData.SupportsDpapi) { try { @@ -80,24 +78,31 @@ namespace Pal.Client.Configuration } else if (Format == EFormat.Unencrypted) return Guid.Parse(EncryptedId); + else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi) + return Guid.Parse(EncryptedId); else return null; } private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g) { - try + if (!ConfigurationData.SupportsDpapi) + return (g.ToString(), null, EFormat.ProtectedDataUnsupported); + else { - byte[] entropy = RandomNumberGenerator.GetBytes(16); - byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser); - return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData); - } - catch (Exception) - { - return (g.ToString(), null, EFormat.Unencrypted); + try + { + byte[] entropy = RandomNumberGenerator.GetBytes(EntropyLength); + byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser); + return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData); + } + catch (Exception) + { + return (g.ToString(), null, EFormat.Unencrypted); + } } } - + public bool EncryptIfNeeded() { if (Format == EFormat.Unencrypted) @@ -111,9 +116,7 @@ namespace Pal.Client.Configuration return true; } } - -#pragma warning disable CS0618 // Type or member is obsolete - if (Format == EFormat.UseProtectedData && ConfigurationData.FixedV1Entropy.SequenceEqual(Entropy ?? Array.Empty())) + else if (Format == EFormat.UseProtectedData && Entropy is { Length: < EntropyLength }) { Guid? g = DecryptAccountId(); if (g != null) @@ -122,7 +125,6 @@ namespace Pal.Client.Configuration return true; } } -#pragma warning restore CS0618 // Type or member is obsolete return false; } @@ -131,6 +133,12 @@ namespace Pal.Client.Configuration { 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 e8cb169..c93339c 100644 --- a/Pal.Client/Configuration/ConfigurationData.cs +++ b/Pal.Client/Configuration/ConfigurationData.cs @@ -1,4 +1,7 @@ -using System; +using Dalamud.Logging; +using System; +using System.Linq; +using System.Security.Cryptography; namespace Pal.Client.Configuration { @@ -6,5 +9,31 @@ namespace Pal.Client.Configuration { [Obsolete("for V1 import")] internal static readonly byte[] FixedV1Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 }; + + private static bool? _supportsDpapi = null; + public static bool SupportsDpapi + { + get + { + if (_supportsDpapi == null) + { + 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; + } + + PluginLog.Verbose($"DPAPI support: {_supportsDpapi}"); + } + return _supportsDpapi.Value; + } + } } } From e7c2cd426b227302ffe35403029332ae8cc367da Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 13:34:44 +0100 Subject: [PATCH 06/38] Config: Clean up --- Pal.Client/Configuration/AccountConfigurationV7.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index de28185..281c68d 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -9,7 +9,7 @@ namespace Pal.Client.Configuration { public class AccountConfigurationV7 : IAccountConfiguration { - private const int EntropyLength = 16; + private const int DefaultEntropyLength = 16; [JsonConstructor] public AccountConfigurationV7() @@ -92,7 +92,7 @@ namespace Pal.Client.Configuration { try { - byte[] entropy = RandomNumberGenerator.GetBytes(EntropyLength); + byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength); byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser); return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData); } @@ -116,7 +116,7 @@ namespace Pal.Client.Configuration return true; } } - else if (Format == EFormat.UseProtectedData && Entropy is { Length: < EntropyLength }) + else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength }) { Guid? g = DecryptAccountId(); if (g != null) From faa35feade5aff515195517e959fe11183ff83ee Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 14:35:11 +0100 Subject: [PATCH 07/38] Config: Few JsonRequired attributes --- .../Configuration/AccountConfigurationV7.cs | 3 + .../Configuration/IPalacePalConfiguration.cs | 5 ++ Pal.Client/DependencyInjection/DIPlugin.cs | 73 +++++++++++++++++++ Pal.Client/Plugin.cs | 11 +-- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 Pal.Client/DependencyInjection/DIPlugin.cs diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index 281c68d..2d74075 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -41,6 +41,7 @@ namespace Pal.Client.Configuration } [JsonInclude] + [JsonRequired] public EFormat Format { get; private set; } = EFormat.Unencrypted; /// @@ -48,11 +49,13 @@ namespace Pal.Client.Configuration /// [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; diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index ecfb8a2..8b10b13 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Numerics; using ImGuiNET; +using Newtonsoft.Json; namespace Pal.Client.Configuration { @@ -59,8 +60,12 @@ namespace Pal.Client.Configuration 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; } } diff --git a/Pal.Client/DependencyInjection/DIPlugin.cs b/Pal.Client/DependencyInjection/DIPlugin.cs new file mode 100644 index 0000000..d64368b --- /dev/null +++ b/Pal.Client/DependencyInjection/DIPlugin.cs @@ -0,0 +1,73 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Plugin; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Properties; + +namespace Pal.Client.DependencyInjection +{ + /// + /// DI-aware Plugin. + /// + internal sealed class DIPlugin : IDalamudPlugin + { + private ServiceProvider? _serviceProvider; + + public string Name => Localization.Palace_Pal; + + public DIPlugin(DalamudPluginInterface pluginInterface, + ClientState clientState, + GameGui gameGui, + ChatGui chatGui, + ObjectTable objectTable, + Framework framework, + Condition condition, + CommandManager commandManager, + DataManager dataManager) + { + IServiceCollection services = new ServiceCollection(); + + // dalamud + services.AddSingleton(this); + services.AddSingleton(pluginInterface); + services.AddSingleton(gameGui); + services.AddSingleton(chatGui); + services.AddSingleton(objectTable); + services.AddSingleton(framework); + services.AddSingleton(condition); + services.AddSingleton(commandManager); + services.AddSingleton(dataManager); + + // palace pal + services.AddSingleton(); + + // build + _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateOnBuild = true, + ValidateScopes = true, + }); + + // initialize plugin + _serviceProvider.GetRequiredService(); + } + + public void Dispose() + { + // ensure we're not calling dispose recursively on ourselves + if (_serviceProvider != null) + { + ServiceProvider serviceProvider = _serviceProvider; + _serviceProvider = null; + + serviceProvider.Dispose(); + } + + } + } +} diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 1e486ac..60f5098 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -33,9 +33,10 @@ using Pal.Client.Net; namespace Pal.Client { - public class Plugin : IDalamudPlugin + public class Plugin : IDisposable { internal const uint ColorInvisible = 0; + private readonly IDalamudPlugin _dalamudPlugin; private LocalizedChatMessages _localizedChatMessages = new(); @@ -51,10 +52,10 @@ namespace Pal.Client internal ConcurrentQueue NextUpdateObjects { get; } = new(); internal IRenderer Renderer { get; private set; } = null!; - public string Name => Localization.Palace_Pal; - - public Plugin(DalamudPluginInterface pluginInterface, ChatGui chat) + public Plugin(DalamudPluginInterface pluginInterface, ChatGui chat, IDalamudPlugin dalamudPlugin) { + _dalamudPlugin = dalamudPlugin; + LanguageChanged(pluginInterface.UiLanguage); PluginLog.Information($"Install source: {pluginInterface.SourceRepository}"); @@ -669,7 +670,7 @@ namespace Pal.Client disposable.Dispose(); if (Service.Configuration.Renderer.SelectedRenderer == ERenderer.Splatoon) - Renderer = new SplatoonRenderer(Service.PluginInterface, this); + Renderer = new SplatoonRenderer(Service.PluginInterface, _dalamudPlugin); else Renderer = new SimpleRenderer(); } From c52341eb0d05863af927c752808cf3af83ea6b2b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 23:17:19 +0100 Subject: [PATCH 08/38] DI: Initial Draft --- Pal.Client/Commands/PalCommand.cs | 141 ++++ .../Configuration/ConfigurationManager.cs | 23 +- Pal.Client/Configuration/ConfigurationV1.cs | 7 +- Pal.Client/Configuration/ConfigurationV7.cs | 10 + .../Configuration/IPalacePalConfiguration.cs | 2 + Pal.Client/DependencyInjection/ChatService.cs | 109 +++ Pal.Client/DependencyInjection/DIPlugin.cs | 61 +- Pal.Client/DependencyInjection/DebugState.cs | 15 + .../DependencyInjection/FloorService.cs | 15 + .../DependencyInjection/FrameworkService.cs | 392 ++++++++++ .../DependencyInjection/RepoVerification.cs | 25 + .../DependencyInjection/StatisticsService.cs | 65 ++ .../DependencyInjection/TerritoryState.cs | 38 + Pal.Client/Hooks.cs | 22 +- Pal.Client/Net/RemoteApi.AccountService.cs | 20 +- Pal.Client/Net/RemoteApi.PalaceService.cs | 1 - Pal.Client/Net/RemoteApi.Utils.cs | 9 - Pal.Client/Net/RemoteApi.cs | 24 +- Pal.Client/Plugin.cs | 694 +----------------- Pal.Client/Rendering/MarkerConfig.cs | 6 +- Pal.Client/Rendering/RenderAdapter.cs | 33 + Pal.Client/Rendering/RenderData.cs | 7 + Pal.Client/Rendering/SimpleRenderer.cs | 153 ++-- Pal.Client/Rendering/SplatoonRenderer.cs | 61 +- .../Scheduled/IQueueOnFrameworkThread.cs | 9 +- Pal.Client/Scheduled/QueueHandler.cs | 203 +++++ Pal.Client/Scheduled/QueuedConfigUpdate.cs | 17 +- Pal.Client/Scheduled/QueuedImport.cs | 84 +-- Pal.Client/Scheduled/QueuedSyncResponse.cs | 75 +- Pal.Client/Scheduled/QueuedUndoImport.cs | 29 +- Pal.Client/Service.cs | 24 +- Pal.Client/Windows/AgreementWindow.cs | 38 +- Pal.Client/Windows/ConfigWindow.cs | 200 +++-- Pal.Client/Windows/StatisticsWindow.cs | 33 +- 34 files changed, 1557 insertions(+), 1088 deletions(-) create mode 100644 Pal.Client/Commands/PalCommand.cs create mode 100644 Pal.Client/DependencyInjection/ChatService.cs create mode 100644 Pal.Client/DependencyInjection/DebugState.cs create mode 100644 Pal.Client/DependencyInjection/FloorService.cs create mode 100644 Pal.Client/DependencyInjection/FrameworkService.cs create mode 100644 Pal.Client/DependencyInjection/RepoVerification.cs create mode 100644 Pal.Client/DependencyInjection/StatisticsService.cs create mode 100644 Pal.Client/DependencyInjection/TerritoryState.cs create mode 100644 Pal.Client/Rendering/RenderAdapter.cs create mode 100644 Pal.Client/Rendering/RenderData.cs create mode 100644 Pal.Client/Scheduled/QueueHandler.cs diff --git a/Pal.Client/Commands/PalCommand.cs b/Pal.Client/Commands/PalCommand.cs new file mode 100644 index 0000000..4d9ff97 --- /dev/null +++ b/Pal.Client/Commands/PalCommand.cs @@ -0,0 +1,141 @@ +using System; +using System.Linq; +using Dalamud.Game.ClientState; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using ECommons.Schedulers; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; +using Pal.Client.Properties; +using Pal.Client.Rendering; +using Pal.Client.Windows; + +namespace Pal.Client.Commands +{ + // should restructure this when more commands exist, if that ever happens + // this command is more-or-less a debug/troubleshooting command, if anything + internal sealed class PalCommand : IDisposable + { + private readonly IPalacePalConfiguration _configuration; + private readonly CommandManager _commandManager; + private readonly ChatGui _chatGui; + private readonly StatisticsService _statisticsService; + private readonly ConfigWindow _configWindow; + private readonly TerritoryState _territoryState; + private readonly FloorService _floorService; + private readonly ClientState _clientState; + + public PalCommand( + IPalacePalConfiguration configuration, + CommandManager commandManager, + ChatGui chatGui, + StatisticsService statisticsService, + ConfigWindow configWindow, + TerritoryState territoryState, + FloorService floorService, + ClientState clientState) + { + _configuration = configuration; + _commandManager = commandManager; + _chatGui = chatGui; + _statisticsService = statisticsService; + _configWindow = configWindow; + _territoryState = territoryState; + _floorService = floorService; + _clientState = clientState; + + _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) + { + HelpMessage = Localization.Command_pal_HelpText + }); + } + + public void Dispose() + { + _commandManager.RemoveHandler("/pal"); + } + + private void OnCommand(string command, string arguments) + { + if (_configuration.FirstUse) + { + _chatGui.PalError(Localization.Error_FirstTimeSetupRequired); + return; + } + + try + { + arguments = arguments.Trim(); + switch (arguments) + { + case "stats": + _statisticsService.ShowGlobalStatistics(); + break; + + case "test-connection": + case "tc": + _configWindow.IsOpen = true; + var _ = new TickScheduler(() => _configWindow.TestConnection()); + break; + +#if DEBUG + case "update-saves": + LocalState.UpdateAll(); + Service.Chat.Print(Localization.Command_pal_updatesaves); + break; +#endif + + case "": + case "config": + _configWindow.Toggle(); + break; + + case "near": + DebugNearest(_ => true); + break; + + case "tnear": + DebugNearest(m => m.Type == Marker.EType.Trap); + break; + + case "hnear": + DebugNearest(m => m.Type == Marker.EType.Hoard); + break; + + default: + _chatGui.PalError(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, + command)); + break; + } + } + catch (Exception e) + { + _chatGui.PalError(e.ToString()); + } + } + + private void DebugNearest(Predicate predicate) + { + if (!_territoryState.IsInDeepDungeon()) + return; + + var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); + var playerPosition = _clientState.LocalPlayer?.Position; + if (playerPosition == null) + return; + _chatGui.Print($"[Palace Pal] {playerPosition}"); + + var nearbyMarkers = state.Markers + .Where(m => predicate(m)) + .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) + .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) + .OrderBy(m => m.distance) + .Take(5) + .ToList(); + foreach (var nearbyMarker in nearbyMarkers) + _chatGui.Print( + $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); + } + } +} diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 86757a7..1dfdbd7 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Text; using System.Text.Encodings.Web; @@ -6,6 +7,8 @@ using System.Text.Json; using Dalamud.Logging; using Dalamud.Plugin; using ImGuiNET; +using Pal.Client.DependencyInjection; +using Pal.Client.Scheduled; using NJson = Newtonsoft.Json; namespace Pal.Client.Configuration @@ -14,12 +17,16 @@ namespace Pal.Client.Configuration { private readonly DalamudPluginInterface _pluginInterface; + public event EventHandler? Saved; + public ConfigurationManager(DalamudPluginInterface pluginInterface) { _pluginInterface = pluginInterface; + + Migrate(); } - public string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); + private string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); public IPalacePalConfiguration Load() { @@ -27,16 +34,20 @@ namespace Pal.Client.Configuration new ConfigurationV7(); } - public void Save(IConfigurationInConfigDirectory config) + public void Save(IConfigurationInConfigDirectory config, bool queue = true) { File.WriteAllText(ConfigPath, - JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), + JsonSerializer.Serialize(config, config.GetType(), + new JsonSerializerOptions + { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), Encoding.UTF8); + if (queue && config is ConfigurationV7 v7) + Saved?.Invoke(this, v7); } #pragma warning disable CS0612 #pragma warning disable CS0618 - public void Migrate() + private void Migrate() { if (_pluginInterface.ConfigFile.Exists) { @@ -49,7 +60,7 @@ namespace Pal.Client.Configuration configurationV1.Save(); var v7 = MigrateToV7(configurationV1); - Save(v7); + Save(v7, queue: false); File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); } diff --git a/Pal.Client/Configuration/ConfigurationV1.cs b/Pal.Client/Configuration/ConfigurationV1.cs index ba47de8..7128f52 100644 --- a/Pal.Client/Configuration/ConfigurationV1.cs +++ b/Pal.Client/Configuration/ConfigurationV1.cs @@ -90,7 +90,7 @@ namespace Pal.Client.Configuration { // 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; + //bool changedAnyFile = false; LocalState.ForEach(s => { foreach (var marker in s.Markers) @@ -104,7 +104,7 @@ namespace Pal.Client.Configuration s.Markers = new ConcurrentBag(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == Marker.EType.Hoard || m.WasImported)); s.Save(); - changedAnyFile = true; + //changedAnyFile = true; } else { @@ -113,6 +113,7 @@ namespace Pal.Client.Configuration } }); + /* // Only notify offline users - we can just re-download the backup markers from the server seamlessly. if (Mode == EMode.Offline && changedAnyFile) { @@ -123,6 +124,7 @@ namespace Pal.Client.Configuration 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(); @@ -144,7 +146,6 @@ namespace Pal.Client.Configuration TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameHandling = TypeNameHandling.Objects })); - Service.Plugin.EarlyEventQueue.Enqueue(new QueuedConfigUpdate()); } public class AccountInfo diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs index 0d2aa52..3fc1dbf 100644 --- a/Pal.Client/Configuration/ConfigurationV7.cs +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Pal.Client.Net; namespace Pal.Client.Configuration; @@ -45,4 +46,13 @@ public class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDi { Accounts.RemoveAll(a => a.Server == server && a.IsUsable); } + + public bool HasRoleOnCurrentServer(string role) + { + if (Mode != EMode.Online) + return false; + + var account = FindAccount(RemoteApi.RemoteUrl); + return account == null || account.CachedRoles.Contains(role); + } } diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index 8b10b13..1c6a747 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -29,6 +29,8 @@ namespace Pal.Client.Configuration IAccountConfiguration CreateAccount(string server, Guid accountId); IAccountConfiguration? FindAccount(string server); void RemoveAccount(string server); + + bool HasRoleOnCurrentServer(string role); } public class DeepDungeonConfiguration diff --git a/Pal.Client/DependencyInjection/ChatService.cs b/Pal.Client/DependencyInjection/ChatService.cs new file mode 100644 index 0000000..2dcfeb2 --- /dev/null +++ b/Pal.Client/DependencyInjection/ChatService.cs @@ -0,0 +1,109 @@ +using System; +using System.Text.RegularExpressions; +using Dalamud.Data; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Lumina.Excel.GeneratedSheets; +using Pal.Client.Configuration; + +namespace Pal.Client.DependencyInjection +{ + 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) + { + _chatGui = chatGui; + _territoryState = territoryState; + _configuration = 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)) + { + _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; + } + } + + 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 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/DIPlugin.cs b/Pal.Client/DependencyInjection/DIPlugin.cs index d64368b..e77e056 100644 --- a/Pal.Client/DependencyInjection/DIPlugin.cs +++ b/Pal.Client/DependencyInjection/DIPlugin.cs @@ -1,13 +1,21 @@ -using Dalamud.Data; +using System.Globalization; +using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Game.Gui; +using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Commands; +using Pal.Client.Configuration; +using Pal.Client.Net; using Pal.Client.Properties; +using Pal.Client.Rendering; +using Pal.Client.Scheduled; +using Pal.Client.Windows; namespace Pal.Client.DependencyInjection { @@ -35,6 +43,7 @@ namespace Pal.Client.DependencyInjection // dalamud services.AddSingleton(this); services.AddSingleton(pluginInterface); + services.AddSingleton(clientState); services.AddSingleton(gameGui); services.AddSingleton(chatGui); services.AddSingleton(objectTable); @@ -42,9 +51,38 @@ namespace Pal.Client.DependencyInjection services.AddSingleton(condition); services.AddSingleton(commandManager); services.AddSingleton(dataManager); + services.AddSingleton(new WindowSystem(typeof(DIPlugin).AssemblyQualifiedName)); - // palace pal + // plugin-specific services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService().Load()); + services.AddTransient(); + services.AddSingleton(); + + // territory handling + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // windows & related services + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + + // these should maybe be scoped + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // set up the current UI language before creating anything + Localization.Culture = new CultureInfo(pluginInterface.UiLanguage); // build _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions @@ -54,6 +92,24 @@ namespace Pal.Client.DependencyInjection }); // initialize plugin +#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(); +#endif + + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); } @@ -67,7 +123,6 @@ namespace Pal.Client.DependencyInjection serviceProvider.Dispose(); } - } } } diff --git a/Pal.Client/DependencyInjection/DebugState.cs b/Pal.Client/DependencyInjection/DebugState.cs new file mode 100644 index 0000000..0a632a3 --- /dev/null +++ b/Pal.Client/DependencyInjection/DebugState.cs @@ -0,0 +1,15 @@ +using System; + +namespace Pal.Client.DependencyInjection +{ + internal class DebugState + { + public string? DebugMessage { get; set; } + + public void SetFromException(Exception e) + => DebugMessage = $"{DateTime.Now}\n{e}"; + + public void Reset() + => DebugMessage = null; + } +} diff --git a/Pal.Client/DependencyInjection/FloorService.cs b/Pal.Client/DependencyInjection/FloorService.cs new file mode 100644 index 0000000..8cdcb98 --- /dev/null +++ b/Pal.Client/DependencyInjection/FloorService.cs @@ -0,0 +1,15 @@ +using System.Collections.Concurrent; + +namespace Pal.Client.DependencyInjection +{ + internal sealed class FloorService + { + public ConcurrentDictionary FloorMarkers { get; } = new(); + public ConcurrentBag EphemeralMarkers { get; set; } = new(); + + public LocalState GetFloorMarkers(ushort territoryType) + { + return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt)); + } + } +} diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs new file mode 100644 index 0000000..f3adaee --- /dev/null +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; +using ImGuiNET; +using Pal.Client.Configuration; +using Pal.Client.Extensions; +using Pal.Client.Net; +using Pal.Client.Rendering; +using Pal.Client.Scheduled; + +namespace Pal.Client.DependencyInjection +{ + internal class FrameworkService : IDisposable + { + 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 QueueHandler _queueHandler; + 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(Framework framework, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + ClientState clientState, + TerritoryState territoryState, + FloorService floorService, + DebugState debugState, + RenderAdapter renderAdapter, + QueueHandler queueHandler, + ObjectTable objectTable, + RemoteApi remoteApi) + { + _framework = framework; + _configurationManager = configurationManager; + _configuration = configuration; + _clientState = clientState; + _territoryState = territoryState; + _floorService = floorService; + _debugState = debugState; + _renderAdapter = renderAdapter; + _queueHandler = queueHandler; + _objectTable = objectTable; + _remoteApi = remoteApi; + + _framework.Update += OnUpdate; + _configurationManager.Saved += OnSaved; + } + + 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 + { + bool recreateLayout = false; + bool saveMarkers = false; + + while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) + _queueHandler.Handle(queued, ref recreateLayout, ref saveMarkers); + + if (_territoryState.LastTerritory != _clientState.TerritoryType) + { + _territoryState.LastTerritory = _clientState.TerritoryType; + _territoryState.TerritorySyncState = SyncState.NotAttempted; + NextUpdateObjects.Clear(); + + if (_territoryState.IsInDeepDungeon()) + _floorService.GetFloorMarkers(_territoryState.LastTerritory); + _floorService.EphemeralMarkers.Clear(); + _territoryState.PomanderOfSight = PomanderState.Inactive; + _territoryState.PomanderOfIntuition = PomanderState.Inactive; + recreateLayout = true; + _debugState.Reset(); + } + + if (!_territoryState.IsInDeepDungeon()) + return; + + if (_configuration.Mode == EMode.Online && _territoryState.TerritorySyncState == SyncState.NotAttempted) + { + _territoryState.TerritorySyncState = SyncState.Started; + Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory)); + } + + while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) + _queueHandler.Handle(queued, ref recreateLayout, ref saveMarkers); + + var currentFloor = _floorService.GetFloorMarkers(_territoryState.LastTerritory); + + IList visibleMarkers = GetRelevantGameObjects(); + HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout); + HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + + #region Render Markers + private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, bool recreateLayout) + { + var currentFloorMarkers = currentFloor.Markers; + + bool updateSeenMarkers = false; + var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); + foreach (var visibleMarker in visibleMarkers) + { + Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); + if (knownMarker != null) + { + if (!knownMarker.Seen) + { + knownMarker.Seen = true; + saveMarkers = true; + } + + // This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, + // markers discovered afterwards are automatically marked seen. + if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && !knownMarker.RemoteSeenOn.Contains(partialAccountId)) + updateSeenMarkers = true; + + continue; + } + + currentFloorMarkers.Add(visibleMarker); + recreateLayout = true; + saveMarkers = true; + } + + if (!recreateLayout && currentFloorMarkers.Count > 0 && (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) + { + + try + { + foreach (var marker in currentFloorMarkers) + { + uint desiredColor = DetermineColor(marker, visibleMarkers); + if (marker.RenderElement == null || !marker.RenderElement.IsValid) + { + recreateLayout = true; + break; + } + + if (marker.RenderElement.Color != desiredColor) + marker.RenderElement.Color = desiredColor; + } + } + catch (Exception e) + { + _debugState.SetFromException(e); + recreateLayout = true; + } + } + + if (updateSeenMarkers && partialAccountId != null) + { + var markersToUpdate = currentFloorMarkers.Where(x => x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && !x.RemoteSeenOn.Contains(partialAccountId)).ToList(); + foreach (var marker in markersToUpdate) + marker.RemoteSeenRequested = true; + Task.Run(async () => await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, markersToUpdate)); + } + + if (saveMarkers) + { + currentFloor.Save(); + + if (_territoryState.TerritorySyncState == SyncState.Complete) + { + var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); + if (markersToUpload.Count > 0) + { + foreach (var marker in markersToUpload) + marker.UploadRequested = true; + Task.Run(async () => await UploadMarkersForTerritory(_territoryState.LastTerritory, markersToUpload)); + } + } + } + + if (recreateLayout) + { + _renderAdapter.ResetLayer(ELayer.TrapHoard); + + List elements = new(); + foreach (var marker in currentFloorMarkers) + { + if (marker.Seen || _configuration.Mode == EMode.Online || marker is { WasImported: true, Imports.Count: > 0 }) + { + if (marker.Type == Marker.EType.Trap) + { + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.Traps); + } + else if (marker.Type == Marker.EType.Hoard) + { + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.HoardCoffers); + } + } + } + + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.TrapHoard, elements); + } + } + + private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) + { + recreateLayout |= _floorService.EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); + recreateLayout |= visibleMarkers.Any(visibleMarker => _floorService.EphemeralMarkers.All(x => x != visibleMarker)); + + if (recreateLayout) + { + _renderAdapter.ResetLayer(ELayer.RegularCoffers); + _floorService.EphemeralMarkers.Clear(); + + List elements = new(); + foreach (var marker in visibleMarkers) + { + _floorService.EphemeralMarkers.Add(marker); + + if (marker.Type == Marker.EType.SilverCoffer && _configuration.DeepDungeons.SilverCoffers.Show) + { + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.SilverCoffers); + } + } + + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.RegularCoffers, elements); + } + } + + private uint DetermineColor(Marker marker, IList visibleMarkers) + { + switch (marker.Type) + { + case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive || !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + return _configuration.DeepDungeons.Traps.Color; + case Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive || !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + return _configuration.DeepDungeons.HoardCoffers.Color; + case Marker.EType.SilverCoffer: + return _configuration.DeepDungeons.SilverCoffers.Color; + case Marker.EType.Trap: + case Marker.EType.Hoard: + return RenderData.ColorInvisible; + default: + return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); + } + } + + private void CreateRenderElement(Marker marker, List elements, uint color, MarkerConfiguration config) + { + if (!config.Show) + return; + + var element = _renderAdapter.CreateElement(marker.Type, marker.Position, color, config.Fill); + marker.RenderElement = element; + elements.Add(element); + } + #endregion + + #region Up-/Download + private async Task DownloadMarkersForTerritory(ushort territoryId) + { + try + { + var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId); + LateEventQueue.Enqueue(new QueuedSyncResponse + { + Type = SyncType.Download, + TerritoryType = territoryId, + Success = success, + Markers = downloadedMarkers + }); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + + private async Task UploadMarkersForTerritory(ushort territoryId, List markersToUpload) + { + try + { + var (success, uploadedMarkers) = await _remoteApi.UploadMarker(territoryId, markersToUpload); + LateEventQueue.Enqueue(new QueuedSyncResponse + { + Type = SyncType.Upload, + TerritoryType = territoryId, + Success = success, + Markers = uploadedMarkers + }); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + + private async Task SyncSeenMarkersForTerritory(ushort territoryId, List markersToUpdate) + { + try + { + var success = await _remoteApi.MarkAsSeen(territoryId, markersToUpdate); + LateEventQueue.Enqueue(new QueuedSyncResponse + { + Type = SyncType.MarkSeen, + TerritoryType = territoryId, + Success = success, + Markers = markersToUpdate, + }); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + #endregion + + private IList GetRelevantGameObjects() + { + List result = new(); + for (int i = 246; i < _objectTable.Length; i++) + { + GameObject? obj = _objectTable[i]; + if (obj == null) + continue; + + switch ((uint)Marshal.ReadInt32(obj.Address + 128)) + { + case 2007182: + case 2007183: + case 2007184: + case 2007185: + case 2007186: + case 2009504: + result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); + break; + + case 2007542: + case 2007543: + result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true }); + break; + + case 2007357: + result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true }); + break; + } + } + + while (NextUpdateObjects.TryDequeue(out nint address)) + { + var obj = _objectTable.FirstOrDefault(x => x.Address == address); + if (obj != null && obj.Position.Length() > 0.1) + result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); + } + + return result; + } + } +} diff --git a/Pal.Client/DependencyInjection/RepoVerification.cs b/Pal.Client/DependencyInjection/RepoVerification.cs new file mode 100644 index 0000000..db46b64 --- /dev/null +++ b/Pal.Client/DependencyInjection/RepoVerification.cs @@ -0,0 +1,25 @@ +using System; +using Dalamud.Game.Gui; +using Dalamud.Logging; +using Dalamud.Plugin; +using Pal.Client.Extensions; +using Pal.Client.Properties; + +namespace Pal.Client.DependencyInjection +{ + public class RepoVerification + { + public RepoVerification(DalamudPluginInterface pluginInterface, ChatGui chatGui) + { + PluginLog.Information($"Install source: {pluginInterface.SourceRepository}"); + if (!pluginInterface.IsDev + && !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/") + && !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/")) + { + chatGui.PalError(string.Format(Localization.Error_WrongRepository, + "https://github.com/carvelli/Dalamud-Plugins")); + throw new InvalidOperationException(); + } + } + } +} diff --git a/Pal.Client/DependencyInjection/StatisticsService.cs b/Pal.Client/DependencyInjection/StatisticsService.cs new file mode 100644 index 0000000..23f2ba9 --- /dev/null +++ b/Pal.Client/DependencyInjection/StatisticsService.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading.Tasks; +using Dalamud.Game.Gui; +using Grpc.Core; +using Pal.Client.Configuration; +using Pal.Client.Extensions; +using Pal.Client.Net; +using Pal.Client.Properties; +using Pal.Client.Windows; + +namespace Pal.Client.DependencyInjection +{ + internal sealed class StatisticsService + { + private readonly IPalacePalConfiguration _configuration; + private readonly RemoteApi _remoteApi; + private readonly StatisticsWindow _statisticsWindow; + private readonly ChatGui _chatGui; + + public StatisticsService(IPalacePalConfiguration configuration, RemoteApi remoteApi, + StatisticsWindow statisticsWindow, ChatGui chatGui) + { + _configuration = configuration; + _remoteApi = remoteApi; + _statisticsWindow = statisticsWindow; + _chatGui = chatGui; + } + + public void ShowGlobalStatistics() + { + Task.Run(async () => await FetchFloorStatistics()); + } + + private async Task FetchFloorStatistics() + { + if (!_configuration.HasRoleOnCurrentServer("statistics:view")) + { + _chatGui.PalError(Localization.Command_pal_stats_CurrentFloor); + return; + } + + try + { + var (success, floorStatistics) = await _remoteApi.FetchStatistics(); + if (success) + { + _statisticsWindow.SetFloorData(floorStatistics); + _statisticsWindow.IsOpen = true; + } + else + { + _chatGui.PalError(Localization.Command_pal_stats_UnableToFetchStatistics); + } + } + catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) + { + _chatGui.Print(Localization.Command_pal_stats_CurrentFloor); + } + catch (Exception e) + { + _chatGui.PalError(e.ToString()); + } + } + } +} diff --git a/Pal.Client/DependencyInjection/TerritoryState.cs b/Pal.Client/DependencyInjection/TerritoryState.cs new file mode 100644 index 0000000..15e21d4 --- /dev/null +++ b/Pal.Client/DependencyInjection/TerritoryState.cs @@ -0,0 +1,38 @@ +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Pal.Client.Scheduled; +using Pal.Common; + +namespace Pal.Client.DependencyInjection +{ + public sealed class TerritoryState + { + private readonly ClientState _clientState; + private readonly Condition _condition; + + public TerritoryState(ClientState clientState, Condition condition) + { + _clientState = clientState; + _condition = condition; + } + + public ushort LastTerritory { get; set; } + public SyncState TerritorySyncState { 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/Hooks.cs b/Pal.Client/Hooks.cs index dca3228..4250075 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -5,11 +5,17 @@ using Dalamud.Memory; using Dalamud.Utility.Signatures; using System; using System.Text; +using Dalamud.Game.ClientState.Objects; +using Pal.Client.DependencyInjection; namespace Pal.Client { - internal unsafe class Hooks + internal unsafe class Hooks : IDisposable { + 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); @@ -17,8 +23,12 @@ namespace Pal.Client private Hook ActorVfxCreateHook { get; init; } = null!; #pragma warning restore CS0649 - public Hooks() + public Hooks(ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) { + _objectTable = objectTable; + _territoryState = territoryState; + _frameworkService = frameworkService; + SignatureHelper.Initialise(this); ActorVfxCreateHook.Enable(); } @@ -55,10 +65,10 @@ namespace Pal.Client { try { - if (Service.Plugin.IsInDeepDungeon()) + if (_territoryState.IsInDeepDungeon()) { var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256); - var obj = Service.ObjectTable.CreateObjectReference(a2); + var obj = _objectTable.CreateObjectReference(a2); /* if (Service.Configuration.BetaKey == "VFX") @@ -69,7 +79,7 @@ namespace Pal.Client { if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx") { - Service.Plugin.NextUpdateObjects.Enqueue(obj.Address); + _frameworkService.NextUpdateObjects.Enqueue(obj.Address); } } } @@ -83,7 +93,7 @@ namespace Pal.Client public void Dispose() { - ActorVfxCreateHook?.Dispose(); + ActorVfxCreateHook.Dispose(); } } } diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index 96dc32e..747709d 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -19,7 +19,7 @@ namespace Pal.Client.Net { private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) { - if (Service.Configuration.Mode != EMode.Online) + if (_configuration.Mode != EMode.Online) { PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection"); return (false, Localization.ConnectionError_NotOnline); @@ -47,7 +47,7 @@ namespace Pal.Client.Net cancellationToken.ThrowIfCancellationRequested(); var accountClient = new AccountService.AccountServiceClient(_channel); - IAccountConfiguration? configuredAccount = Service.Configuration.FindAccount(RemoteUrl); + IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl); if (configuredAccount == null) { PluginLog.Information($"TryConnect: No account information saved for {RemoteUrl}, creating new account"); @@ -57,17 +57,17 @@ namespace Pal.Client.Net if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) throw new InvalidOperationException("invalid account id returned"); - configuredAccount = Service.Configuration.CreateAccount(RemoteUrl, accountId); + configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); PluginLog.Information($"TryConnect: Account created with id {accountId.ToPartialId()}"); - Service.ConfigurationManager.Save(Service.Configuration); + _configurationManager.Save(_configuration); } else { PluginLog.Error($"TryConnect: Account creation failed with error {createAccountReply.Error}"); if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) { - Service.Chat.PalError(Localization.ConnectionError_OldVersion); + _chatGui.PalError(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error)); @@ -102,7 +102,7 @@ namespace Pal.Client.Net } if (save) - Service.ConfigurationManager.Save(Service.Configuration); + _configurationManager.Save(_configuration); } else { @@ -110,8 +110,8 @@ namespace Pal.Client.Net _loginInfo = new LoginInfo(null); if (loginReply.Error == LoginError.InvalidAccountId) { - Service.Configuration.RemoveAccount(RemoteUrl); - Service.ConfigurationManager.Save(Service.Configuration); + _configuration.RemoveAccount(RemoteUrl); + _configurationManager.Save(_configuration); if (retry) { PluginLog.Information("TryConnect: Attempting connection retry without account id"); @@ -122,7 +122,7 @@ namespace Pal.Client.Net } if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) { - Service.Chat.PalError(Localization.ConnectionError_OldVersion); + _chatGui.PalError(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error)); @@ -161,7 +161,7 @@ namespace Pal.Client.Net return Localization.ConnectionSuccessful; } - internal class LoginInfo + internal sealed class LoginInfo { public LoginInfo(string? authToken) { diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs index cee5337..259b1ea 100644 --- a/Pal.Client/Net/RemoteApi.PalaceService.cs +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/Pal.Client/Net/RemoteApi.Utils.cs b/Pal.Client/Net/RemoteApi.Utils.cs index 0ff7af8..045adb8 100644 --- a/Pal.Client/Net/RemoteApi.Utils.cs +++ b/Pal.Client/Net/RemoteApi.Utils.cs @@ -53,14 +53,5 @@ namespace Pal.Client.Net return null; #endif } - - public bool HasRoleOnCurrentServer(string role) - { - if (Service.Configuration.Mode != Configuration.EMode.Online) - return false; - - var account = Service.Configuration.FindAccount(RemoteUrl); - return account == null || account.CachedRoles.Contains(role); - } } } diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 1f7d6d9..9315941 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -2,28 +2,40 @@ using Grpc.Net.Client; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; -using Pal.Client.Extensions; +using Dalamud.Game.Gui; using Pal.Client.Configuration; namespace Pal.Client.Net { - internal partial class RemoteApi : IDisposable + internal sealed partial class RemoteApi : IDisposable { #if DEBUG public const string RemoteUrl = "http://localhost:5145"; #else 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 _grpcToPluginLogLoggerFactory = LoggerFactory.Create(builder => builder.AddProvider(new GrpcLoggerProvider()).AddFilter("Grpc", LogLevel.Trace)); + private readonly ILoggerFactory _grpcToPluginLogLoggerFactory = LoggerFactory.Create(builder => + builder.AddProvider(new GrpcLoggerProvider()).AddFilter("Grpc", LogLevel.Trace)); + + private readonly ChatGui _chatGui; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; private GrpcChannel? _channel; private LoginInfo _loginInfo = new(null); private bool _warnedAboutUpgrade; + public RemoteApi(ChatGui chatGui, ConfigurationManager configurationManager, + IPalacePalConfiguration configuration) + { + _chatGui = chatGui; + _configurationManager = configurationManager; + _configuration = configuration; + } + public void Dispose() { PluginLog.Debug("Disposing gRPC channel"); diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 60f5098..8d1449b 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -1,709 +1,87 @@ -using Dalamud.Game; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Plugin; -using Grpc.Core; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using Pal.Client.Rendering; using Pal.Client.Scheduled; using Pal.Client.Windows; -using Pal.Common; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Numerics; using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Threading.Tasks; +using Dalamud.Logging; using Pal.Client.Extensions; using Pal.Client.Properties; using ECommons; -using ECommons.Schedulers; +using Microsoft.Extensions.DependencyInjection; using Pal.Client.Configuration; -using Pal.Client.Net; namespace Pal.Client { - public class Plugin : IDisposable + internal sealed class Plugin : IDisposable { - internal const uint ColorInvisible = 0; - private readonly IDalamudPlugin _dalamudPlugin; + private readonly IServiceProvider _serviceProvider; + private readonly DalamudPluginInterface _pluginInterface; + private readonly IPalacePalConfiguration _configuration; + private readonly RenderAdapter _renderAdapter; - private LocalizedChatMessages _localizedChatMessages = new(); - - internal ConcurrentDictionary FloorMarkers { get; } = new(); - internal ConcurrentBag EphemeralMarkers { get; set; } = new(); - internal ushort LastTerritory { get; set; } - internal SyncState TerritorySyncState { get; set; } - internal PomanderState PomanderOfSight { get; private set; } = PomanderState.Inactive; - internal PomanderState PomanderOfIntuition { get; private set; } = PomanderState.Inactive; - internal string? DebugMessage { get; set; } - internal Queue EarlyEventQueue { get; } = new(); - internal Queue LateEventQueue { get; } = new(); - internal ConcurrentQueue NextUpdateObjects { get; } = new(); - internal IRenderer Renderer { get; private set; } = null!; - - public Plugin(DalamudPluginInterface pluginInterface, ChatGui chat, IDalamudPlugin dalamudPlugin) + public Plugin( + IServiceProvider serviceProvider, + DalamudPluginInterface pluginInterface, + IPalacePalConfiguration configuration, + RenderAdapter renderAdapter) { - _dalamudPlugin = dalamudPlugin; + PluginLog.Information("Initializing Palace Pal"); + + _serviceProvider = serviceProvider; + _pluginInterface = pluginInterface; + _configuration = configuration; + _renderAdapter = renderAdapter; + + // initialize legacy services + pluginInterface.Create(); + Service.Configuration = configuration; LanguageChanged(pluginInterface.UiLanguage); - PluginLog.Information($"Install source: {pluginInterface.SourceRepository}"); - -#if RELEASE - // You're welcome to remove this code in your fork, as long as: - // - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and - // - you host your own server instance - if (!pluginInterface.IsDev - && !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/") - && !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/")) - { - chat.PalError(string.Format(Localization.Error_WrongRepository, "https://github.com/carvelli/Dalamud-Plugins")); - throw new InvalidOperationException(); - } -#endif - - pluginInterface.Create(); - Service.Plugin = this; - - Service.ConfigurationManager = new(pluginInterface); - Service.ConfigurationManager.Migrate(); - Service.Configuration = Service.ConfigurationManager.Load(); - - ResetRenderer(); - - Service.Hooks = new Hooks(); - - var agreementWindow = pluginInterface.Create(); - if (agreementWindow is not null) - { - agreementWindow.IsOpen = Service.Configuration.FirstUse; - Service.WindowSystem.AddWindow(agreementWindow); - } - - var configWindow = pluginInterface.Create(); - if (configWindow is not null) - { - Service.WindowSystem.AddWindow(configWindow); - } - - var statisticsWindow = pluginInterface.Create(); - if (statisticsWindow is not null) - { - Service.WindowSystem.AddWindow(statisticsWindow); - } - pluginInterface.UiBuilder.Draw += Draw; pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; pluginInterface.LanguageChanged += LanguageChanged; - Service.Framework.Update += OnFrameworkUpdate; - Service.Chat.ChatMessage += OnChatMessage; - Service.CommandManager.AddHandler("/pal", new CommandInfo(OnCommand) - { - HelpMessage = Localization.Command_pal_HelpText - }); - - ReloadLanguageStrings(); } private void OpenConfigUi() { - Window? configWindow; - if (Service.Configuration.FirstUse) - configWindow = Service.WindowSystem.GetWindow(); + Window configWindow; + if (_configuration.FirstUse) + configWindow = _serviceProvider.GetRequiredService(); else - configWindow = Service.WindowSystem.GetWindow(); + configWindow = _serviceProvider.GetRequiredService(); - if (configWindow != null) - configWindow.IsOpen = true; - } - - private void OnCommand(string command, string arguments) - { - if (Service.Configuration.FirstUse) - { - Service.Chat.PalError(Localization.Error_FirstTimeSetupRequired); - return; - } - - try - { - arguments = arguments.Trim(); - switch (arguments) - { - case "stats": - Task.Run(async () => await FetchFloorStatistics()); - break; - - case "test-connection": - case "tc": - var configWindow = Service.WindowSystem.GetWindow(); - if (configWindow == null) - return; - - configWindow.IsOpen = true; - var _ = new TickScheduler(() => configWindow.TestConnection()); - break; - -#if DEBUG - case "update-saves": - LocalState.UpdateAll(); - Service.Chat.Print(Localization.Command_pal_updatesaves); - break; -#endif - - case "": - case "config": - Service.WindowSystem.GetWindow()?.Toggle(); - break; - - case "near": - DebugNearest(_ => true); - break; - - case "tnear": - DebugNearest(m => m.Type == Marker.EType.Trap); - break; - - case "hnear": - DebugNearest(m => m.Type == Marker.EType.Hoard); - break; - - default: - Service.Chat.PalError(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, command)); - break; - } - } - catch (Exception e) - { - Service.Chat.PalError(e.ToString()); - } + configWindow.IsOpen = true; } #region IDisposable Support - protected virtual void Dispose(bool disposing) - { - if (!disposing) return; - - Service.CommandManager.RemoveHandler("/pal"); - Service.PluginInterface.UiBuilder.Draw -= Draw; - Service.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; - Service.PluginInterface.LanguageChanged -= LanguageChanged; - Service.Framework.Update -= OnFrameworkUpdate; - Service.Chat.ChatMessage -= OnChatMessage; - - Service.WindowSystem.GetWindow()?.Dispose(); - Service.WindowSystem.RemoveAllWindows(); - - Service.RemoteApi.Dispose(); - Service.Hooks.Dispose(); - - if (Renderer is IDisposable disposable) - disposable.Dispose(); - } - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; + _pluginInterface.LanguageChanged -= LanguageChanged; } #endregion - private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, ref bool isHandled) + private void LanguageChanged(string languageCode) { - if (Service.Configuration.FirstUse) - return; - - if (type != (XivChatType)2105) - return; - - string message = seMessage.ToString(); - if (_localizedChatMessages.FloorChanged.IsMatch(message)) - { - PomanderOfSight = PomanderState.Inactive; - - if (PomanderOfIntuition == PomanderState.FoundOnCurrentFloor) - PomanderOfIntuition = PomanderState.Inactive; - } - else if (message.EndsWith(_localizedChatMessages.MapRevealed)) - { - PomanderOfSight = PomanderState.Active; - } - else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved)) - { - 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. - PomanderOfIntuition = PomanderState.Active; - } - else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened)) - { - PomanderOfIntuition = PomanderState.FoundOnCurrentFloor; - } - } - - private void LanguageChanged(string langcode) - { - Localization.Culture = new CultureInfo(langcode); - Service.WindowSystem.Windows.OfType().Each(w => w.LanguageChanged()); - } - - private void OnFrameworkUpdate(Framework framework) - { - if (Service.Configuration.FirstUse) - return; - - try - { - bool recreateLayout = false; - bool saveMarkers = false; - - while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - queued.Run(this, ref recreateLayout, ref saveMarkers); - - if (LastTerritory != Service.ClientState.TerritoryType) - { - LastTerritory = Service.ClientState.TerritoryType; - TerritorySyncState = SyncState.NotAttempted; - NextUpdateObjects.Clear(); - - if (IsInDeepDungeon()) - GetFloorMarkers(LastTerritory); - EphemeralMarkers.Clear(); - PomanderOfSight = PomanderState.Inactive; - PomanderOfIntuition = PomanderState.Inactive; - recreateLayout = true; - DebugMessage = null; - } - - if (!IsInDeepDungeon()) - return; - - if (Service.Configuration.Mode == Configuration.EMode.Online && TerritorySyncState == SyncState.NotAttempted) - { - TerritorySyncState = SyncState.Started; - Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory)); - } - - while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - queued.Run(this, ref recreateLayout, ref saveMarkers); - - var currentFloor = GetFloorMarkers(LastTerritory); - - IList visibleMarkers = GetRelevantGameObjects(); - HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout); - HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - - internal LocalState GetFloorMarkers(ushort territoryType) - { - return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt)); - } - - #region Rendering markers - private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, bool recreateLayout) - { - var config = Service.Configuration; - var currentFloorMarkers = currentFloor.Markers; - - bool updateSeenMarkers = false; - var partialAccountId = Service.Configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - foreach (var visibleMarker in visibleMarkers) - { - Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); - if (knownMarker != null) - { - if (!knownMarker.Seen) - { - knownMarker.Seen = true; - saveMarkers = true; - } - - // This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, - // markers discovered afterwards are automatically marked seen. - if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && !knownMarker.RemoteSeenOn.Contains(partialAccountId)) - updateSeenMarkers = true; - - continue; - } - - currentFloorMarkers.Add(visibleMarker); - recreateLayout = true; - saveMarkers = true; - } - - if (!recreateLayout && currentFloorMarkers.Count > 0 && (config.DeepDungeons.Traps.OnlyVisibleAfterPomander || config.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) - { - - try - { - foreach (var marker in currentFloorMarkers) - { - uint desiredColor = DetermineColor(marker, visibleMarkers); - if (marker.RenderElement == null || !marker.RenderElement.IsValid) - { - recreateLayout = true; - break; - } - - if (marker.RenderElement.Color != desiredColor) - marker.RenderElement.Color = desiredColor; - } - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - recreateLayout = true; - } - } - - if (updateSeenMarkers && partialAccountId != null) - { - var markersToUpdate = currentFloorMarkers.Where(x => x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && !x.RemoteSeenOn.Contains(partialAccountId)).ToList(); - foreach (var marker in markersToUpdate) - marker.RemoteSeenRequested = true; - Task.Run(async () => await SyncSeenMarkersForTerritory(LastTerritory, markersToUpdate)); - } - - if (saveMarkers) - { - currentFloor.Save(); - - if (TerritorySyncState == SyncState.Complete) - { - var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); - if (markersToUpload.Count > 0) - { - foreach (var marker in markersToUpload) - marker.UploadRequested = true; - Task.Run(async () => await UploadMarkersForTerritory(LastTerritory, markersToUpload)); - } - } - } - - if (recreateLayout) - { - Renderer.ResetLayer(ELayer.TrapHoard); - - List elements = new(); - foreach (var marker in currentFloorMarkers) - { - if (marker.Seen || config.Mode == EMode.Online || marker is { WasImported: true, Imports.Count: > 0 }) - { - if (marker.Type == Marker.EType.Trap) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.Traps); - } - else if (marker.Type == Marker.EType.Hoard) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.HoardCoffers); - } - } - } - - if (elements.Count == 0) - return; - - Renderer.SetLayer(ELayer.TrapHoard, elements); - } - } - - private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) - { - recreateLayout |= EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); - recreateLayout |= visibleMarkers.Any(visibleMarker => EphemeralMarkers.All(x => x != visibleMarker)); - - if (recreateLayout) - { - Renderer.ResetLayer(ELayer.RegularCoffers); - EphemeralMarkers.Clear(); - - var config = Service.Configuration; - - List elements = new(); - foreach (var marker in visibleMarkers) - { - EphemeralMarkers.Add(marker); - - if (marker.Type == Marker.EType.SilverCoffer && config.DeepDungeons.SilverCoffers.Show) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.SilverCoffers); - } - } - - if (elements.Count == 0) - return; - - Renderer.SetLayer(ELayer.RegularCoffers, elements); - } - } - - private uint DetermineColor(Marker marker, IList visibleMarkers) - { - switch (marker.Type) - { - case Marker.EType.Trap when PomanderOfSight == PomanderState.Inactive || !Service.Configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): - return Service.Configuration.DeepDungeons.Traps.Color; - case Marker.EType.Hoard when PomanderOfIntuition == PomanderState.Inactive || !Service.Configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): - return Service.Configuration.DeepDungeons.HoardCoffers.Color; - case Marker.EType.SilverCoffer: - return Service.Configuration.DeepDungeons.SilverCoffers.Color; - case Marker.EType.Trap: - case Marker.EType.Hoard: - return ColorInvisible; - default: - return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); - } - } - - private void CreateRenderElement(Marker marker, List elements, uint color, MarkerConfiguration config) - { - if (!config.Show) - return; - - var element = Renderer.CreateElement(marker.Type, marker.Position, color, config.Fill); - marker.RenderElement = element; - elements.Add(element); - } - #endregion - - #region Up-/Download - private async Task DownloadMarkersForTerritory(ushort territoryId) - { - try - { - var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId); - LateEventQueue.Enqueue(new QueuedSyncResponse - { - Type = SyncType.Download, - TerritoryType = territoryId, - Success = success, - Markers = downloadedMarkers - }); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - - private async Task UploadMarkersForTerritory(ushort territoryId, List markersToUpload) - { - try - { - var (success, uploadedMarkers) = await Service.RemoteApi.UploadMarker(territoryId, markersToUpload); - LateEventQueue.Enqueue(new QueuedSyncResponse - { - Type = SyncType.Upload, - TerritoryType = territoryId, - Success = success, - Markers = uploadedMarkers - }); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - - private async Task SyncSeenMarkersForTerritory(ushort territoryId, List markersToUpdate) - { - try - { - var success = await Service.RemoteApi.MarkAsSeen(territoryId, markersToUpdate); - LateEventQueue.Enqueue(new QueuedSyncResponse - { - Type = SyncType.MarkSeen, - TerritoryType = territoryId, - Success = success, - Markers = markersToUpdate, - }); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - #endregion - - #region Command Handling - private async Task FetchFloorStatistics() - { - if (!Service.RemoteApi.HasRoleOnCurrentServer("statistics:view")) - { - Service.Chat.PalError(Localization.Command_pal_stats_CurrentFloor); - return; - } - - try - { - var (success, floorStatistics) = await Service.RemoteApi.FetchStatistics(); - if (success) - { - var statisticsWindow = Service.WindowSystem.GetWindow()!; - statisticsWindow.SetFloorData(floorStatistics); - statisticsWindow.IsOpen = true; - } - else - { - Service.Chat.PalError(Localization.Command_pal_stats_UnableToFetchStatistics); - } - } - catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) - { - Service.Chat.Print(Localization.Command_pal_stats_CurrentFloor); - } - catch (Exception e) - { - Service.Chat.PalError(e.ToString()); - } - } - - private void DebugNearest(Predicate predicate) - { - if (!IsInDeepDungeon()) - return; - - var state = GetFloorMarkers(Service.ClientState.TerritoryType); - var playerPosition = Service.ClientState.LocalPlayer?.Position; - if (playerPosition == null) - return; - Service.Chat.Print($"[Palace Pal] {playerPosition}"); - - var nearbyMarkers = state.Markers - .Where(m => predicate(m)) - .Where(m => m.RenderElement != null && m.RenderElement.Color != ColorInvisible) - .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) - .OrderBy(m => m.distance) - .Take(5) - .ToList(); - foreach (var nearbyMarker in nearbyMarkers) - Service.Chat.Print($"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); - } - #endregion - - private IList GetRelevantGameObjects() - { - List result = new(); - for (int i = 246; i < Service.ObjectTable.Length; i++) - { - GameObject? obj = Service.ObjectTable[i]; - if (obj == null) - continue; - - switch ((uint)Marshal.ReadInt32(obj.Address + 128)) - { - case 2007182: - case 2007183: - case 2007184: - case 2007185: - case 2007186: - case 2009504: - result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); - break; - - case 2007542: - case 2007543: - result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true }); - break; - - case 2007357: - result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true }); - break; - } - } - - while (NextUpdateObjects.TryDequeue(out nint address)) - { - var obj = Service.ObjectTable.FirstOrDefault(x => x.Address == address); - if (obj != null && obj.Position.Length() > 0.1) - result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); - } - - return result; - } - - internal bool IsInDeepDungeon() => - Service.ClientState.IsLoggedIn - && Service.Condition[ConditionFlag.InDeepDungeon] - && typeof(ETerritoryType).IsEnumDefined(Service.ClientState.TerritoryType); - - private void ReloadLanguageStrings() - { - _localizedChatMessages = 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+)") + "$"), - }; - } - - internal void ResetRenderer() - { - if (Renderer is SplatoonRenderer && Service.Configuration.Renderer.SelectedRenderer == ERenderer.Splatoon) - return; - else if (Renderer is SimpleRenderer && Service.Configuration.Renderer.SelectedRenderer == ERenderer.Simple) - return; - - if (Renderer is IDisposable disposable) - disposable.Dispose(); - - if (Service.Configuration.Renderer.SelectedRenderer == ERenderer.Splatoon) - Renderer = new SplatoonRenderer(Service.PluginInterface, _dalamudPlugin); - else - Renderer = new SimpleRenderer(); + Localization.Culture = new CultureInfo(languageCode); + _serviceProvider.GetRequiredService().Windows.OfType().Each(w => w.LanguageChanged()); } private void Draw() { - if (Renderer is SimpleRenderer sr) + if (_renderAdapter.Implementation is SimpleRenderer sr) sr.DrawLayers(); - Service.WindowSystem.Draw(); - } - - private string GetLocalizedString(uint id) - { - return Service.DataManager.GetExcelSheet()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; - } - - public enum PomanderState - { - Inactive, - Active, - FoundOnCurrentFloor, - PomanderOfSafetyUsed, - } - - private 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+)$"); + _serviceProvider.GetRequiredService().Draw(); } } } diff --git a/Pal.Client/Rendering/MarkerConfig.cs b/Pal.Client/Rendering/MarkerConfig.cs index 58d3642..2ef9dde 100644 --- a/Pal.Client/Rendering/MarkerConfig.cs +++ b/Pal.Client/Rendering/MarkerConfig.cs @@ -2,7 +2,7 @@ namespace Pal.Client.Rendering { - internal class MarkerConfig + internal sealed class MarkerConfig { private static readonly MarkerConfig EmptyConfig = new(); private static readonly Dictionary MarkerConfigs = new() @@ -12,8 +12,8 @@ namespace Pal.Client.Rendering { Marker.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, }; - public float OffsetY { get; set; } - public float Radius { get; set; } = 0.25f; + public float OffsetY { get; private init; } + public float Radius { get; private init; } = 0.25f; public static MarkerConfig ForType(Marker.EType type) => MarkerConfigs.GetValueOrDefault(type, EmptyConfig); } diff --git a/Pal.Client/Rendering/RenderAdapter.cs b/Pal.Client/Rendering/RenderAdapter.cs new file mode 100644 index 0000000..da8a28f --- /dev/null +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Numerics; +using Pal.Client.Configuration; + +namespace Pal.Client.Rendering +{ + internal sealed class RenderAdapter : IRenderer + { + private readonly SimpleRenderer _simpleRenderer; + private readonly SplatoonRenderer _splatoonRenderer; + private readonly IPalacePalConfiguration _configuration; + + public RenderAdapter(SimpleRenderer simpleRenderer, SplatoonRenderer splatoonRenderer, IPalacePalConfiguration configuration) + { + _simpleRenderer = simpleRenderer; + _splatoonRenderer = splatoonRenderer; + _configuration = configuration; + } + + public IRenderer Implementation => _configuration.Renderer.SelectedRenderer == ERenderer.Splatoon + ? _splatoonRenderer + : _simpleRenderer; + + public void SetLayer(ELayer layer, IReadOnlyList elements) + => Implementation.SetLayer(layer, elements); + + public void ResetLayer(ELayer layer) + => Implementation.ResetLayer(layer); + + public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) + => Implementation.CreateElement(type, pos, color, fill); + } +} diff --git a/Pal.Client/Rendering/RenderData.cs b/Pal.Client/Rendering/RenderData.cs new file mode 100644 index 0000000..2c4b802 --- /dev/null +++ b/Pal.Client/Rendering/RenderData.cs @@ -0,0 +1,7 @@ +namespace Pal.Client.Rendering +{ + internal static class RenderData + { + public static readonly uint ColorInvisible = 0; + } +} diff --git a/Pal.Client/Rendering/SimpleRenderer.cs b/Pal.Client/Rendering/SimpleRenderer.cs index 58ed62c..675426a 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -1,15 +1,14 @@ -using Dalamud.Game.Gui; -using Dalamud.Interface; -using Dalamud.Plugin; -using ECommons.ExcelServices.TerritoryEnumeration; +using Dalamud.Interface; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Xml.Linq; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client.Rendering { @@ -20,15 +19,30 @@ namespace Pal.Client.Rendering /// remade into PalacePal (which is the third or fourth iteration on the same idea /// I made, just with a clear vision). /// - internal class SimpleRenderer : IRenderer, IDisposable + 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) + { + _clientState = clientState; + _gameGui = gameGui; + _configuration = configuration; + _territoryState = territoryState; + } + public void SetLayer(ELayer layer, IReadOnlyList elements) { _layers[layer] = new SimpleLayer { - TerritoryType = Service.ClientState.TerritoryType, + TerritoryType = _clientState.TerritoryType, Elements = elements.Cast().ToList() }; } @@ -61,38 +75,88 @@ namespace Pal.Client.Rendering 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)) + if (ImGui.Begin("###PalacePalSimpleRender", + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | + ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.AlwaysUseWindowPadding)) { - ushort territoryType = Service.ClientState.TerritoryType; + ushort territoryType = _clientState.TerritoryType; foreach (var layer in _layers.Values.Where(l => l.TerritoryType == territoryType)) - layer.Draw(); + { + foreach (var e in layer.Elements) + Draw(e); + } - foreach (var key in _layers.Where(l => l.Value.TerritoryType != territoryType).Select(l => l.Key).ToList()) + foreach (var key in _layers.Where(l => l.Value.TerritoryType != territoryType).Select(l => l.Key) + .ToList()) ResetLayer(key); ImGui.End(); } + ImGui.PopStyleVar(); } + private void Draw(SimpleElement e) + { + if (e.Color == RenderData.ColorInvisible) + return; + + switch (e.Type) + { + case Marker.EType.Hoard: + // ignore distance if this is a found hoard coffer + if (_territoryState.PomanderOfIntuition == PomanderState.Active && + _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) + break; + + goto case Marker.EType.Trap; + + case Marker.EType.Trap: + var playerPos = _clientState.LocalPlayer?.Position; + if (playerPos == null) + return; + + if ((playerPos.Value - e.Position).Length() > 65) + return; + 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); + + 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 void Dispose() { foreach (var l in _layers.Values) l.Dispose(); } - public class SimpleLayer : IDisposable + public sealed class SimpleLayer : IDisposable { public required ushort TerritoryType { get; init; } public required IReadOnlyList Elements { get; init; } - public void Draw() - { - foreach (var element in Elements) - element.Draw(); - } - public void Dispose() { foreach (var e in Elements) @@ -100,63 +164,14 @@ namespace Pal.Client.Rendering } } - public class SimpleElement : IRenderElement + public sealed class SimpleElement : IRenderElement { - private const int SegmentCount = 20; - public bool IsValid { get; set; } = true; public required Marker.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; } - - public void Draw() - { - if (Color == Plugin.ColorInvisible) - return; - - switch (Type) - { - case Marker.EType.Hoard: - // ignore distance if this is a found hoard coffer - if (Service.Plugin.PomanderOfIntuition == Plugin.PomanderState.Active && Service.Configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) - break; - - goto case Marker.EType.Trap; - - case Marker.EType.Trap: - var playerPos = Service.ClientState.LocalPlayer?.Position; - if (playerPos == null) - return; - - if ((playerPos.Value - Position).Length() > 65) - return; - break; - } - - bool onScreen = false; - for (int index = 0; index < 2 * SegmentCount; ++index) - { - onScreen |= Service.GameGui.WorldToScreen(new Vector3( - Position.X + Radius * (float)Math.Sin(Math.PI / SegmentCount * index), - Position.Y, - Position.Z + Radius * (float)Math.Cos(Math.PI / SegmentCount * index)), - out Vector2 vector2); - - ImGui.GetWindowDrawList().PathLineTo(vector2); - } - - if (onScreen) - { - if (Fill) - ImGui.GetWindowDrawList().PathFillConvex(Color); - else - ImGui.GetWindowDrawList().PathStroke(Color, ImDrawFlags.Closed, 2); - } - else - ImGui.GetWindowDrawList().PathClear(); - } } } } diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 2c07578..2fc5c02 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -11,21 +11,33 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Reflection; -using System.Text; -using System.Threading.Tasks; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; +using Pal.Client.DependencyInjection; namespace Pal.Client.Rendering { - internal class SplatoonRenderer : IRenderer, IDrawDebugItems, IDisposable + internal sealed class SplatoonRenderer : IRenderer, IDrawDebugItems, IDisposable { private const long OnTerritoryChange = -2; - private bool IsDisposed { get; set; } - public SplatoonRenderer(DalamudPluginInterface pluginInterface, IDalamudPlugin plugin) + private readonly DebugState _debugState; + private readonly ClientState _clientState; + private readonly ChatGui _chatGui; + + public SplatoonRenderer(DalamudPluginInterface pluginInterface, IDalamudPlugin dalamudPlugin, DebugState debugState, + ClientState clientState, ChatGui chatGui) { - ECommonsMain.Init(pluginInterface, plugin, ECommons.Module.SplatoonAPI); + _debugState = debugState; + _clientState = clientState; + _chatGui = chatGui; + + PluginLog.Information("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 @@ -33,12 +45,14 @@ namespace Pal.Client.Rendering { try { - Splatoon.AddDynamicElements(ToLayerName(layer), elements.Cast().Select(x => x.Delegate).ToArray(), new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange }); + Splatoon.AddDynamicElements(ToLayerName(layer), + elements.Cast().Select(x => x.Delegate).ToArray(), + new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange }); } catch (Exception e) { PluginLog.Error(e, $"Could not create splatoon layer {layer} with {elements.Count} elements"); - Service.Plugin.DebugMessage = $"{DateTime.Now}\n{e}"; + _debugState.SetFromException(e); } }); } @@ -82,7 +96,7 @@ namespace Pal.Client.Rendering { try { - Vector3? pos = Service.ClientState.LocalPlayer?.Position; + Vector3? pos = _clientState.LocalPlayer?.Position; if (pos != null) { var elements = new List @@ -91,9 +105,11 @@ namespace Pal.Client.Rendering CreateElement(Marker.EType.Hoard, pos.Value, ImGui.ColorConvertFloat4ToU32(hoardColor)), }; - if (!Splatoon.AddDynamicElements("PalacePal.Test", elements.Cast().Select(x => x.Delegate).ToArray(), new[] { Environment.TickCount64 + 10000 })) + if (!Splatoon.AddDynamicElements("PalacePal.Test", + elements.Cast().Select(x => x.Delegate).ToArray(), + new[] { Environment.TickCount64 + 10000 })) { - Service.Chat.PrintError("Could not draw markers :("); + _chatGui.PrintError("Could not draw markers :("); } } } @@ -102,23 +118,31 @@ namespace Pal.Client.Rendering try { var pluginManager = DalamudReflector.GetPluginManager(); - IList installedPlugins = pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ?? new List(); + 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); + AssemblyName? assemblyName = + (AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t); string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t); if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon") { - Service.Chat.PrintError($"[Palace Pal] Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); - Service.Chat.Print("[Palace Pal] You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); + _chatGui.PrintError( + $"[Palace Pal] Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); + _chatGui.Print( + "[Palace Pal] You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); return; } } } - catch (Exception) { } + catch (Exception) + { + // not relevant + } - Service.Chat.PrintError("Could not draw markers, is Splatoon installed and enabled?"); + _chatGui.PrintError("Could not draw markers, is Splatoon installed and enabled?"); } } @@ -132,7 +156,7 @@ namespace Pal.Client.Rendering ECommonsMain.Dispose(); } - public class SplatoonElement : IRenderElement + private sealed class SplatoonElement : IRenderElement { private readonly SplatoonRenderer _renderer; @@ -145,6 +169,7 @@ namespace Pal.Client.Rendering public Element Delegate { get; } public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid(); + public uint Color { get => Delegate.color; diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index bf02b9d..d04446f 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Pal.Client.Scheduled +namespace Pal.Client.Scheduled { internal interface IQueueOnFrameworkThread { - void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers); } } diff --git a/Pal.Client/Scheduled/QueueHandler.cs b/Pal.Client/Scheduled/QueueHandler.cs new file mode 100644 index 0000000..8ec67ef --- /dev/null +++ b/Pal.Client/Scheduled/QueueHandler.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.Gui; +using Dalamud.Logging; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; +using Pal.Client.Net; +using Pal.Client.Properties; +using Pal.Common; + +namespace Pal.Client.Scheduled +{ + // TODO The idea was to split this from the queue objects, should be in individual classes tho + internal sealed class QueueHandler + { + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; + private readonly FloorService _floorService; + private readonly TerritoryState _territoryState; + private readonly DebugState _debugState; + private readonly ChatGui _chatGui; + + public QueueHandler( + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + FloorService floorService, + TerritoryState territoryState, + DebugState debugState, + ChatGui chatGui) + { + _configurationManager = configurationManager; + _configuration = configuration; + _floorService = floorService; + _territoryState = territoryState; + _debugState = debugState; + _chatGui = chatGui; + } + + public void Handle(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) + { + if (queued is QueuedConfigUpdate) + { + ConfigUpdate(ref recreateLayout, ref saveMarkers); + } + else if (queued is QueuedSyncResponse queuedSyncResponse) + { + SyncResponse(queuedSyncResponse); + recreateLayout = true; + saveMarkers = true; + } + else if (queued is QueuedImport queuedImport) + { + Import(queuedImport); + recreateLayout = true; + saveMarkers = true; + } + else if (queued is QueuedUndoImport queuedUndoImport) + { + UndoImport(queuedUndoImport); + recreateLayout = true; + saveMarkers = true; + } + else + throw new InvalidOperationException(); + } + + private void ConfigUpdate(ref bool recreateLayout, ref bool saveMarkers) + { + if (_configuration.Mode == EMode.Offline) + { + LocalState.UpdateAll(); + _floorService.FloorMarkers.Clear(); + _floorService.EphemeralMarkers.Clear(); + _territoryState.LastTerritory = 0; + + recreateLayout = true; + saveMarkers = true; + } + } + + private void SyncResponse(QueuedSyncResponse queued) + { + try + { + var remoteMarkers = queued.Markers; + var currentFloor = _floorService.GetFloorMarkers(queued.TerritoryType); + if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) + { + switch (queued.Type) + { + case SyncType.Download: + case SyncType.Upload: + foreach (var remoteMarker in remoteMarkers) + { + // Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved. + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + { + localMarker.NetworkId = remoteMarker.NetworkId; + continue; + } + + if (queued.Type == SyncType.Download) + currentFloor.Markers.Add(remoteMarker); + } + + break; + + case SyncType.MarkSeen: + var partialAccountId = + _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); + if (partialAccountId == null) + break; + foreach (var remoteMarker in remoteMarkers) + { + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + localMarker.RemoteSeenOn.Add(partialAccountId); + } + + break; + } + } + + // don't modify state for outdated floors + if (_territoryState.LastTerritory != queued.TerritoryType) + return; + + if (queued.Type == SyncType.Download) + { + if (queued.Success) + _territoryState.TerritorySyncState = SyncState.Complete; + else + _territoryState.TerritorySyncState = SyncState.Failed; + } + } + catch (Exception e) + { + _debugState.SetFromException(e); + if (queued.Type == SyncType.Download) + _territoryState.TerritorySyncState = SyncState.Failed; + } + } + + private void Import(QueuedImport queued) + { + try + { + if (!queued.Validate(_chatGui)) + return; + + var oldExportIds = string.IsNullOrEmpty(queued.Export.ServerUrl) + ? _configuration.ImportHistory.Where(x => x.RemoteUrl == queued.Export.ServerUrl).Select(x => x.Id) + .Where(x => x != Guid.Empty).ToList() + : new List(); + + foreach (var remoteFloor in queued.Export.Floors) + { + ushort territoryType = (ushort)remoteFloor.TerritoryType; + var localState = _floorService.GetFloorMarkers(territoryType); + + localState.UndoImport(oldExportIds); + queued.ImportFloor(remoteFloor, localState); + + localState.Save(); + } + + _configuration.ImportHistory.RemoveAll(hist => + oldExportIds.Contains(hist.Id) || hist.Id == queued.ExportId); + _configuration.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry + { + Id = queued.ExportId, + RemoteUrl = queued.Export.ServerUrl, + ExportedAt = queued.Export.CreatedAt.ToDateTime(), + ImportedAt = DateTime.UtcNow, + }); + _configurationManager.Save(_configuration); + + _chatGui.Print(string.Format(Localization.ImportCompleteStatistics, queued.ImportedTraps, + queued.ImportedHoardCoffers)); + } + catch (Exception e) + { + PluginLog.Error(e, "Import failed"); + _chatGui.PalError(string.Format(Localization.Error_ImportFailed, e)); + } + } + + private void UndoImport(QueuedUndoImport queued) + { + foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) + { + var localState = _floorService.GetFloorMarkers((ushort)territoryType); + localState.UndoImport(new List { queued.ExportId }); + localState.Save(); + } + + _configuration.ImportHistory.RemoveAll(hist => hist.Id == queued.ExportId); + } + } +} diff --git a/Pal.Client/Scheduled/QueuedConfigUpdate.cs b/Pal.Client/Scheduled/QueuedConfigUpdate.cs index d816ec1..0536362 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -1,21 +1,6 @@ namespace Pal.Client.Scheduled { - internal class QueuedConfigUpdate : IQueueOnFrameworkThread + internal sealed class QueuedConfigUpdate : IQueueOnFrameworkThread { - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) - { - if (Service.Configuration.Mode == Configuration.EMode.Offline) - { - LocalState.UpdateAll(); - plugin.FloorMarkers.Clear(); - plugin.EphemeralMarkers.Clear(); - plugin.LastTerritory = 0; - - recreateLayout = true; - saveMarkers = true; - } - - plugin.ResetRenderer(); - } } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 45623d8..4a74c79 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -1,98 +1,54 @@ using Account; -using Dalamud.Logging; using Pal.Common; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using Pal.Client.Extensions; +using Dalamud.Game.Gui; using Pal.Client.Properties; -using Pal.Client.Configuration; namespace Pal.Client.Scheduled { - internal class QueuedImport : IQueueOnFrameworkThread + internal sealed class QueuedImport : IQueueOnFrameworkThread { - private readonly ExportRoot _export; - private Guid _exportId; - private int _importedTraps; - private int _importedHoardCoffers; + public ExportRoot Export { get; } + public Guid ExportId { get; private set; } + public int ImportedTraps { get; private set; } + public int ImportedHoardCoffers { get; private set; } public QueuedImport(string sourcePath) { using var input = File.OpenRead(sourcePath); - _export = ExportRoot.Parser.ParseFrom(input); + Export = ExportRoot.Parser.ParseFrom(input); } - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) + public bool Validate(ChatGui chatGui) { - try + if (Export.ExportVersion != ExportConfig.ExportVersion) { - if (!Validate()) - return; - - var config = Service.Configuration; - var oldExportIds = string.IsNullOrEmpty(_export.ServerUrl) ? config.ImportHistory.Where(x => x.RemoteUrl == _export.ServerUrl).Select(x => x.Id).Where(x => x != Guid.Empty).ToList() : new List(); - - foreach (var remoteFloor in _export.Floors) - { - ushort territoryType = (ushort)remoteFloor.TerritoryType; - var localState = plugin.GetFloorMarkers(territoryType); - - localState.UndoImport(oldExportIds); - ImportFloor(remoteFloor, localState); - - localState.Save(); - } - - config.ImportHistory.RemoveAll(hist => oldExportIds.Contains(hist.Id) || hist.Id == _exportId); - config.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry - { - Id = _exportId, - RemoteUrl = _export.ServerUrl, - ExportedAt = _export.CreatedAt.ToDateTime(), - ImportedAt = DateTime.UtcNow, - }); - Service.ConfigurationManager.Save(config); - - recreateLayout = true; - saveMarkers = true; - - Service.Chat.Print(string.Format(Localization.ImportCompleteStatistics, _importedTraps, _importedHoardCoffers)); - } - catch (Exception e) - { - PluginLog.Error(e, "Import failed"); - Service.Chat.PalError(string.Format(Localization.Error_ImportFailed, e)); - } - } - - private bool Validate() - { - if (_export.ExportVersion != ExportConfig.ExportVersion) - { - Service.Chat.PrintError(Localization.Error_ImportFailed_IncompatibleVersion); + chatGui.PrintError(Localization.Error_ImportFailed_IncompatibleVersion); return false; } - if (!Guid.TryParse(_export.ExportId, out _exportId) || _exportId == Guid.Empty) + if (!Guid.TryParse(Export.ExportId, out Guid exportId) || ExportId == Guid.Empty) { - Service.Chat.PrintError(Localization.Error_ImportFailed_InvalidFile); + chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); return false; } - if (string.IsNullOrEmpty(_export.ServerUrl)) + ExportId = exportId; + + if (string.IsNullOrEmpty(Export.ServerUrl)) { // If we allow for backups as import/export, this should be removed - Service.Chat.PrintError(Localization.Error_ImportFailed_InvalidFile); + chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); return false; } return true; } - private void ImportFloor(ExportFloor remoteFloor, LocalState localState) + public void ImportFloor(ExportFloor remoteFloor, LocalState localState) { var remoteMarkers = remoteFloor.Objects.Select(m => new Marker((Marker.EType)m.Type, new Vector3(m.X, m.Y, m.Z)) { WasImported = true }); foreach (var remoteMarker in remoteMarkers) @@ -104,12 +60,12 @@ namespace Pal.Client.Scheduled localMarker = remoteMarker; if (localMarker.Type == Marker.EType.Trap) - _importedTraps++; + ImportedTraps++; else if (localMarker.Type == Marker.EType.Hoard) - _importedHoardCoffers++; + ImportedHoardCoffers++; } - remoteMarker.Imports.Add(_exportId); + remoteMarker.Imports.Add(ExportId); } } } diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index 44a88a6..db94177 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -1,84 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Pal.Client.Extensions; -using Pal.Client.Net; -using static Pal.Client.Plugin; +using System.Collections.Generic; namespace Pal.Client.Scheduled { - internal 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 List Markers { get; init; } - - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) - { - recreateLayout = true; - saveMarkers = true; - - try - { - var remoteMarkers = Markers; - var currentFloor = plugin.GetFloorMarkers(TerritoryType); - if (Service.Configuration.Mode == Configuration.EMode.Online && Success && remoteMarkers.Count > 0) - { - switch (Type) - { - case SyncType.Download: - case SyncType.Upload: - foreach (var remoteMarker in remoteMarkers) - { - // Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved. - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - { - localMarker.NetworkId = remoteMarker.NetworkId; - continue; - } - - if (Type == SyncType.Download) - currentFloor.Markers.Add(remoteMarker); - } - break; - - case SyncType.MarkSeen: - var partialAccountId = Service.Configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - if (partialAccountId == null) - break; - foreach (var remoteMarker in remoteMarkers) - { - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - localMarker.RemoteSeenOn.Add(partialAccountId); - } - break; - } - } - - // don't modify state for outdated floors - if (plugin.LastTerritory != TerritoryType) - return; - - if (Type == SyncType.Download) - { - if (Success) - plugin.TerritorySyncState = SyncState.Complete; - else - plugin.TerritorySyncState = SyncState.Failed; - } - } - catch (Exception e) - { - plugin.DebugMessage = $"{DateTime.Now}\n{e}"; - if (Type == SyncType.Download) - plugin.TerritorySyncState = SyncState.Failed; - } - } } public enum SyncState diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index 1c0163e..961532a 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -1,35 +1,14 @@ -using ECommons.Configuration; -using Pal.Common; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System; namespace Pal.Client.Scheduled { - internal class QueuedUndoImport : IQueueOnFrameworkThread + internal sealed class QueuedUndoImport : IQueueOnFrameworkThread { - private readonly Guid _exportId; - public QueuedUndoImport(Guid exportId) { - _exportId = exportId; + ExportId = exportId; } - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) - { - recreateLayout = true; - saveMarkers = true; - - foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) - { - var localState = plugin.GetFloorMarkers((ushort)territoryType); - localState.UndoImport(new List { _exportId }); - localState.Save(); - } - - Service.Configuration.ImportHistory.RemoveAll(hist => hist.Id == _exportId); - } + public Guid ExportId { get; } } } diff --git a/Pal.Client/Service.cs b/Pal.Client/Service.cs index 4b54075..b5d0dc5 100644 --- a/Pal.Client/Service.cs +++ b/Pal.Client/Service.cs @@ -1,35 +1,15 @@ -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Interface.Windowing; +using System; using Dalamud.IoC; using Dalamud.Plugin; using Pal.Client.Configuration; -using Pal.Client.Net; namespace Pal.Client { + [Obsolete] public class Service { [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService] public static ClientState ClientState { get; set; } = null!; - [PluginService] public static ChatGui Chat { get; private set; } = null!; - [PluginService] public static ObjectTable ObjectTable { get; private set; } = null!; - [PluginService] public static Framework Framework { get; set; } = null!; - [PluginService] public static Condition Condition { get; set; } = null!; - [PluginService] public static CommandManager CommandManager { get; set; } = null!; - [PluginService] public static DataManager DataManager { get; set; } = null!; - [PluginService] public static GameGui GameGui { get; set; } = null!; - internal static Plugin Plugin { get; set; } = null!; - internal static WindowSystem WindowSystem { get; } = new(typeof(Service).AssemblyQualifiedName); - internal static RemoteApi RemoteApi { get; } = new(); - internal static ConfigurationManager ConfigurationManager { get; set; } = null!; internal static IPalacePalConfiguration Configuration { get; set; } = null!; - internal static Hooks Hooks { get; set; } = null!; } } diff --git a/Pal.Client/Windows/AgreementWindow.cs b/Pal.Client/Windows/AgreementWindow.cs index 41fb25c..316ee49 100644 --- a/Pal.Client/Windows/AgreementWindow.cs +++ b/Pal.Client/Windows/AgreementWindow.cs @@ -1,19 +1,32 @@ -using Dalamud.Interface.Colors; +using System; +using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using ECommons; using ImGuiNET; using System.Numerics; +using Pal.Client.Configuration; using Pal.Client.Properties; namespace Pal.Client.Windows { - internal class AgreementWindow : Window, 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() : base(WindowId) + public AgreementWindow( + WindowSystem windowSystem, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration) + : base(WindowId) { + _windowSystem = windowSystem; + _configurationManager = configurationManager; + _configuration = configuration; + LanguageChanged(); Flags = ImGuiWindowFlags.NoCollapse; @@ -27,8 +40,14 @@ namespace Pal.Client.Windows MinimumSize = new Vector2(500, 500), MaximumSize = new Vector2(2000, 2000), }; + + IsOpen = configuration.FirstUse; + _windowSystem.AddWindow(this); } + public void Dispose() + => _windowSystem.RemoveWindow(this); + public void LanguageChanged() => WindowName = $"{Localization.Palace_Pal}{WindowId}"; @@ -39,8 +58,6 @@ namespace Pal.Client.Windows public override void Draw() { - var config = Service.Configuration; - ImGui.TextWrapped(Localization.Explanation_1); ImGui.TextWrapped(Localization.Explanation_2); @@ -49,8 +66,8 @@ namespace Pal.Client.Windows ImGui.TextWrapped(Localization.Explanation_3); ImGui.TextWrapped(Localization.Explanation_4); - ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice, (int)Configuration.EMode.Online); - ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice, (int)Configuration.EMode.Offline); + ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice, (int)EMode.Online); + ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice, (int)EMode.Offline); ImGui.Separator(); @@ -67,12 +84,13 @@ namespace Pal.Client.Windows ImGui.BeginDisabled(_choice == -1); if (ImGui.Button(Localization.Agreement_UsingThisOnMyOwnRisk)) { - config.Mode = (Configuration.EMode)_choice; - config.FirstUse = false; - Service.ConfigurationManager.Save(config); + _configuration.Mode = (EMode)_choice; + _configuration.FirstUse = false; + _configurationManager.Save(_configuration); IsOpen = false; } + ImGui.EndDisabled(); ImGui.Separator(); diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 535f67e..e9fb03e 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -18,14 +18,28 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Dalamud.Game.Gui; using Pal.Client.Properties; using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client.Windows { - internal class ConfigWindow : Window, ILanguageChanged, IDisposable + internal sealed class ConfigWindow : Window, ILanguageChanged, IDisposable { private const string WindowId = "###PalPalaceConfig"; + + 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 ChatGui _chatGui; + private readonly RemoteApi _remoteApi; + private int _mode; private int _renderer; private ConfigurableMarker _trapConfig = new(); @@ -43,8 +57,30 @@ namespace Pal.Client.Windows private CancellationTokenSource? _testConnectionCts; - public ConfigWindow() : base(WindowId) + public ConfigWindow( + WindowSystem windowSystem, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + RenderAdapter renderAdapter, + TerritoryState territoryState, + FrameworkService frameworkService, + FloorService floorService, + DebugState debugState, + ChatGui chatGui, + RemoteApi remoteApi) + : base(WindowId) { + _windowSystem = windowSystem; + _configurationManager = configurationManager; + _configuration = configuration; + _renderAdapter = renderAdapter; + _territoryState = territoryState; + _frameworkService = frameworkService; + _floorService = floorService; + _debugState = debugState; + _chatGui = chatGui; + _remoteApi = remoteApi; + LanguageChanged(); Size = new Vector2(500, 400); @@ -52,8 +88,18 @@ namespace Pal.Client.Windows Position = new Vector2(300, 300); PositionCondition = ImGuiCond.FirstUseEver; - _importDialog = new FileDialogManager { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; - _exportDialog = new FileDialogManager { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + _importDialog = new FileDialogManager + { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + _exportDialog = new FileDialogManager + { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + + _windowSystem.AddWindow(this); + } + + public void Dispose() + { + _windowSystem.RemoveWindow(this); + _testConnectionCts?.Cancel(); } public void LanguageChanged() @@ -62,19 +108,13 @@ namespace Pal.Client.Windows WindowName = $"{Localization.Palace_Pal} v{version}{WindowId}"; } - public void Dispose() - { - _testConnectionCts?.Cancel(); - } - public override void OnOpen() { - var config = Service.Configuration; - _mode = (int)config.Mode; - _renderer = (int)config.Renderer.SelectedRenderer; - _trapConfig = new ConfigurableMarker(config.DeepDungeons.Traps); - _hoardConfig = new ConfigurableMarker(config.DeepDungeons.HoardCoffers); - _silverConfig = new ConfigurableMarker(config.DeepDungeons.SilverCoffers); + _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); _connectionText = null; } @@ -106,14 +146,13 @@ namespace Pal.Client.Windows if (save || saveAndClose) { - var config = Service.Configuration; - config.Mode = (EMode)_mode; - config.Renderer.SelectedRenderer = (ERenderer)_renderer; - config.DeepDungeons.Traps = _trapConfig.Build(); - config.DeepDungeons.HoardCoffers = _hoardConfig.Build(); - config.DeepDungeons.SilverCoffers = _silverConfig.Build(); + _configuration.Mode = (EMode)_mode; + _configuration.Renderer.SelectedRenderer = (ERenderer)_renderer; + _configuration.DeepDungeons.Traps = _trapConfig.Build(); + _configuration.DeepDungeons.HoardCoffers = _hoardConfig.Build(); + _configuration.DeepDungeons.SilverCoffers = _silverConfig.Build(); - Service.ConfigurationManager.Save(config); + _configurationManager.Save(_configuration); if (saveAndClose) IsOpen = false; @@ -141,8 +180,10 @@ namespace Pal.Client.Windows 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.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(); @@ -155,7 +196,8 @@ namespace Pal.Client.Windows ImGui.Indent(); ImGui.BeginDisabled(!_silverConfig.Show); ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverConfig.Color, ImGuiColorEditFlags.NoInputs); + ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverConfig.Color, + ImGuiColorEditFlags.NoInputs); ImGui.Checkbox(Localization.Config_SilverCoffer_Filled, ref _silverConfig.Fill); ImGui.EndDisabled(); ImGui.Unindent(); @@ -172,7 +214,8 @@ namespace Pal.Client.Windows private void DrawCommunityTab(ref bool saveAndClose) { - if (BeginTabItemEx($"{Localization.ConfigTab_Community}###TabCommunity", _switchToCommunityTab ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) + if (BeginTabItemEx($"{Localization.ConfigTab_Community}###TabCommunity", + _switchToCommunityTab ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) { _switchToCommunityTab = false; @@ -180,12 +223,13 @@ namespace Pal.Client.Windows ImGui.TextWrapped(Localization.Explanation_4); ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _mode, (int)EMode.Online); - ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, (int)EMode.Offline); + ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, + (int)EMode.Offline); saveAndClose = ImGui.Button(Localization.SaveAndClose); ImGui.Separator(); - ImGui.BeginDisabled(Service.Configuration.Mode != EMode.Online); + ImGui.BeginDisabled(_configuration.Mode != EMode.Online); if (ImGui.Button(Localization.Config_TestConnection)) TestConnection(); @@ -205,7 +249,8 @@ namespace Pal.Client.Windows 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/")); + 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(); @@ -215,14 +260,16 @@ namespace Pal.Client.Windows ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) { - _importDialog.OpenFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", (success, paths) => - { - if (success && paths.Count == 1) + _importDialog.OpenFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", + (success, paths) => { - _openImportPath = paths.First(); - } - }, selectionCountMax: 1, startPath: _openImportDialogStartPath, isModal: false); - _openImportDialogStartPath = null; // only use this once, FileDialogManager will save path between calls + 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)); @@ -230,11 +277,13 @@ namespace Pal.Client.Windows DoImport(_openImportPath); ImGui.EndDisabled(); - var importHistory = Service.Configuration.ImportHistory.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefault(); + var importHistory = _configuration.ImportHistory.OrderByDescending(x => x.ImportedAt) + .ThenBy(x => x.Id).FirstOrDefault(); if (importHistory != null) { ImGui.Separator(); - ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, importHistory.ImportedAt, importHistory.RemoteUrl, importHistory.ExportedAt)); + ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, + importHistory.ImportedAt, importHistory.RemoteUrl, importHistory.ExportedAt)); ImGui.TextWrapped(Localization.Config_UndoImportExplanation2); if (ImGui.Button(Localization.Config_UndoImport)) UndoImport(importHistory.Id); @@ -246,7 +295,8 @@ namespace Pal.Client.Windows private void DrawExportTab() { - if (Service.RemoteApi.HasRoleOnCurrentServer("export:run") && ImGui.BeginTabItem($"{Localization.ConfigTab_Export}###TabExport")) + if (_configuration.HasRoleOnCurrentServer("export:run") && + ImGui.BeginTabItem($"{Localization.ConfigTab_Export}###TabExport")) { string todaysFileName = $"export-{DateTime.Today:yyyy-MM-dd}.pal"; if (string.IsNullOrEmpty(_saveExportPath) && !string.IsNullOrEmpty(_saveExportDialogStartPath)) @@ -259,14 +309,16 @@ namespace Pal.Client.Windows 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)) + _importDialog.SaveFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", + todaysFileName, "pal", (success, path) => { - _saveExportPath = path; - } - }, startPath: _saveExportDialogStartPath, isModal: false); - _saveExportDialogStartPath = null; // only use this once, FileDialogManager will save path between calls + 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)); @@ -283,8 +335,11 @@ namespace Pal.Client.Windows 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.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(); @@ -294,9 +349,9 @@ namespace Pal.Client.Windows ImGui.Separator(); ImGui.Text(Localization.Config_Splatoon_Test); - ImGui.BeginDisabled(!(Service.Plugin.Renderer is IDrawDebugItems)); + ImGui.BeginDisabled(!(_renderAdapter.Implementation is IDrawDebugItems)); if (ImGui.Button(Localization.Config_Splatoon_DrawCircles)) - (Service.Plugin.Renderer as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, _hoardConfig.Color); + (_renderAdapter.Implementation as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, _hoardConfig.Color); ImGui.EndDisabled(); ImGui.EndTabItem(); @@ -307,39 +362,43 @@ namespace Pal.Client.Windows { if (ImGui.BeginTabItem($"{Localization.ConfigTab_Debug}###TabDebug")) { - var plugin = Service.Plugin; - if (plugin.IsInDeepDungeon()) + if (_territoryState.IsInDeepDungeon()) { - ImGui.Text($"You are in a deep dungeon, territory type {plugin.LastTerritory}."); - ImGui.Text($"Sync State = {plugin.TerritorySyncState}"); - ImGui.Text($"{plugin.DebugMessage}"); + ImGui.Text($"You are in a deep dungeon, territory type {_territoryState.LastTerritory}."); + ImGui.Text($"Sync State = {_territoryState.TerritorySyncState}"); + ImGui.Text($"{_debugState.DebugMessage}"); ImGui.Indent(); - if (plugin.FloorMarkers.TryGetValue(plugin.LastTerritory, out var currentFloor)) + if (_floorService.FloorMarkers.TryGetValue(_territoryState.LastTerritory, out var currentFloor)) { if (_trapConfig.Show) { int traps = currentFloor.Markers.Count(x => x.Type == Marker.EType.Trap); ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); } + if (_hoardConfig.Show) { int hoardCoffers = currentFloor.Markers.Count(x => x.Type == Marker.EType.Hoard); ImGui.Text($"{hoardCoffers} known hoard coffer{(hoardCoffers == 1 ? "" : "s")}"); } + if (_silverConfig.Show) { - int silverCoffers = plugin.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); - ImGui.Text($"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); + int silverCoffers = _floorService.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); + ImGui.Text( + $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); } - ImGui.Text($"Pomander of Sight: {plugin.PomanderOfSight}"); - ImGui.Text($"Pomander of Intuition: {plugin.PomanderOfIntuition}"); + ImGui.Text($"Pomander of Sight: {_territoryState.PomanderOfSight}"); + ImGui.Text($"Pomander of Intuition: {_territoryState.PomanderOfIntuition}"); } 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)."); + 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); @@ -378,7 +437,7 @@ namespace Pal.Client.Windows try { - _connectionText = await Service.RemoteApi.VerifyConnection(cts.Token); + _connectionText = await _remoteApi.VerifyConnection(cts.Token); } catch (Exception e) { @@ -388,19 +447,20 @@ namespace Pal.Client.Windows _connectionText = e.ToString(); } else - PluginLog.Warning(e, "Could not establish a remote connection, but user also clicked 'test connection' again so not updating UI"); + PluginLog.Warning(e, + "Could not establish a remote connection, but user also clicked 'test connection' again so not updating UI"); } }); } private void DoImport(string sourcePath) { - Service.Plugin.EarlyEventQueue.Enqueue(new QueuedImport(sourcePath)); + _frameworkService.EarlyEventQueue.Enqueue(new QueuedImport(sourcePath)); } private void UndoImport(Guid importId) { - Service.Plugin.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); + _frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); } private void DoExport(string destinationPath) @@ -409,28 +469,28 @@ namespace Pal.Client.Windows { try { - (bool success, ExportRoot export) = await Service.RemoteApi.DoExport(); + (bool success, ExportRoot export) = await _remoteApi.DoExport(); if (success) { await using var output = File.Create(destinationPath); export.WriteTo(output); - Service.Chat.Print($"Export saved as {destinationPath}."); + _chatGui.Print($"Export saved as {destinationPath}."); } else { - Service.Chat.PrintError("Export failed due to server error."); + _chatGui.PrintError("Export failed due to server error."); } } catch (Exception e) { PluginLog.Error(e, "Export failed"); - Service.Chat.PrintError($"Export failed: {e}"); + _chatGui.PrintError($"Export failed: {e}"); } }); } - private class ConfigurableMarker + private sealed class ConfigurableMarker { public bool Show; public Vector4 Color; diff --git a/Pal.Client/Windows/StatisticsWindow.cs b/Pal.Client/Windows/StatisticsWindow.cs index 03eabdc..05f953a 100644 --- a/Pal.Client/Windows/StatisticsWindow.cs +++ b/Pal.Client/Windows/StatisticsWindow.cs @@ -13,13 +13,17 @@ using System.Reflection; namespace Pal.Client.Windows { - internal class StatisticsWindow : Window, ILanguageChanged + internal class StatisticsWindow : Window, IDisposable, ILanguageChanged { private const string WindowId = "###PalacePalStats"; + private readonly WindowSystem _windowSystem; private readonly SortedDictionary _territoryStatistics = new(); - public StatisticsWindow() : base(WindowId) + public StatisticsWindow(WindowSystem windowSystem) + : base(WindowId) { + _windowSystem = windowSystem; + LanguageChanged(); Size = new Vector2(500, 500); @@ -30,8 +34,13 @@ namespace Pal.Client.Windows { _territoryStatistics[territory] = new TerritoryStatistics(territory.ToString()); } + + _windowSystem.AddWindow(this); } + public void Dispose() + => _windowSystem.RemoveWindow(this); + public void LanguageChanged() => WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}"; @@ -39,8 +48,10 @@ namespace Pal.Client.Windows { 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("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); } } @@ -48,7 +59,8 @@ namespace Pal.Client.Windows { 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); @@ -56,7 +68,9 @@ namespace Pal.Client.Windows 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)) + 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()) @@ -71,8 +85,10 @@ namespace Pal.Client.Windows if (ImGui.TableNextColumn()) ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-"); } + ImGui.EndTable(); } + ImGui.EndTabItem(); } } @@ -87,7 +103,8 @@ namespace Pal.Client.Windows foreach (var floor in floorStatistics) { - if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, out TerritoryStatistics? territoryStatistics)) + if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, + out TerritoryStatistics? territoryStatistics)) { territoryStatistics.TrapCount = floor.TrapCount; territoryStatistics.HoardCofferCount = floor.HoardCount; @@ -97,7 +114,7 @@ namespace Pal.Client.Windows private class TerritoryStatistics { - public string TerritoryName { get; set; } + public string TerritoryName { get; } public uint? TrapCount { get; set; } public uint? HoardCofferCount { get; set; } From 29aefee135ab7e7789e1b871f691fa41faa7b192 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 23:51:35 +0100 Subject: [PATCH 09/38] DI: Fix migration, cleanup --- .../Configuration/AccountConfigurationV7.cs | 3 +- .../Configuration/ConfigurationManager.cs | 13 +-- Pal.Client/Configuration/ConfigurationV1.cs | 26 +++--- Pal.Client/Configuration/ConfigurationV7.cs | 87 ++++++++++--------- Pal.Client/DependencyInjection/DebugState.cs | 2 +- ...lugin.cs => DependencyInjectionContext.cs} | 7 +- .../DependencyInjection/FrameworkService.cs | 2 +- Pal.Client/Hooks.cs | 2 +- Pal.Client/LocalState.cs | 4 +- Pal.Client/Marker.cs | 2 +- Pal.Client/Net/GrpcLogger.cs | 2 +- Pal.Client/Net/GrpcLoggerProvider.cs | 2 +- Pal.Client/Net/JwtClaims.cs | 8 +- Pal.Client/Plugin.cs | 8 +- Pal.Client/Windows/StatisticsWindow.cs | 4 +- 15 files changed, 83 insertions(+), 89 deletions(-) rename Pal.Client/DependencyInjection/{DIPlugin.cs => DependencyInjectionContext.cs} (93%) diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index 2d74075..4c65342 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography; using System.Text.Json.Serialization; using Dalamud.Logging; namespace Pal.Client.Configuration { - public class AccountConfigurationV7 : IAccountConfiguration + public sealed class AccountConfigurationV7 : IAccountConfiguration { private const int DefaultEntropyLength = 16; diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 1dfdbd7..ac2b808 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -7,13 +7,11 @@ using System.Text.Json; using Dalamud.Logging; using Dalamud.Plugin; using ImGuiNET; -using Pal.Client.DependencyInjection; -using Pal.Client.Scheduled; using NJson = Newtonsoft.Json; namespace Pal.Client.Configuration { - internal class ConfigurationManager + internal sealed class ConfigurationManager { private readonly DalamudPluginInterface _pluginInterface; @@ -56,13 +54,13 @@ namespace Pal.Client.Configuration ConfigurationV1 configurationV1 = NJson.JsonConvert.DeserializeObject( File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1(); - configurationV1.Migrate(); - configurationV1.Save(); + configurationV1.Migrate(_pluginInterface); + configurationV1.Save(_pluginInterface); var v7 = MigrateToV7(configurationV1); Save(v7, queue: false); - File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); + //File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); } } @@ -81,18 +79,21 @@ namespace Pal.Client.Configuration { 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 } } diff --git a/Pal.Client/Configuration/ConfigurationV1.cs b/Pal.Client/Configuration/ConfigurationV1.cs index 7128f52..c824f33 100644 --- a/Pal.Client/Configuration/ConfigurationV1.cs +++ b/Pal.Client/Configuration/ConfigurationV1.cs @@ -1,19 +1,17 @@ using Dalamud.Logging; -using ECommons.Schedulers; using Newtonsoft.Json; -using Pal.Client.Scheduled; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using Pal.Client.Extensions; +using Dalamud.Plugin; namespace Pal.Client.Configuration { [Obsolete] - public class ConfigurationV1 + public sealed class ConfigurationV1 { public int Version { get; set; } = 6; @@ -52,7 +50,7 @@ namespace Pal.Client.Configuration public string BetaKey { get; set; } = ""; #endregion - public void Migrate() + public void Migrate(DalamudPluginInterface pluginInterface) { if (Version == 1) { @@ -65,7 +63,7 @@ namespace Pal.Client.Configuration AccountIds["https://pal.μ.tv"] = accountId; Version = 2; - Save(); + Save(pluginInterface); } if (Version == 2) @@ -77,13 +75,13 @@ namespace Pal.Client.Configuration Id = x.Value.ToString() // encryption happens in V7 migration at latest }); Version = 3; - Save(); + Save(pluginInterface); } if (Version == 3) { Version = 4; - Save(); + Save(pluginInterface); } if (Version == 4) @@ -127,7 +125,7 @@ namespace Pal.Client.Configuration */ Version = 5; - Save(); + Save(pluginInterface); } if (Version == 5) @@ -135,26 +133,26 @@ namespace Pal.Client.Configuration LocalState.UpdateAll(); Version = 6; - Save(); + Save(pluginInterface); } } - public void Save() + public void Save(DalamudPluginInterface pluginInterface) { - File.WriteAllText(Service.PluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings + File.WriteAllText(pluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings { TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameHandling = TypeNameHandling.Objects })); } - public class AccountInfo + public sealed class AccountInfo { public string? Id { get; set; } public List CachedRoles { get; set; } = new(); } - public class ImportHistoryEntry + public sealed class ImportHistoryEntry { public Guid Id { get; set; } public string? RemoteUrl { get; set; } diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs index 3fc1dbf..9c2012f 100644 --- a/Pal.Client/Configuration/ConfigurationV7.cs +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -4,55 +4,56 @@ using System.Linq; using System.Text.Json.Serialization; using Pal.Client.Net; -namespace Pal.Client.Configuration; - -public class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDirectory +namespace Pal.Client.Configuration { - 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(); - - [JsonIgnore] - [Obsolete] - public List ImportHistory { get; set; } = new(); - - public IAccountConfiguration CreateAccount(string server, Guid accountId) + public sealed class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDirectory { - var account = new AccountConfigurationV7(server, accountId); - Accounts.Add(account); - return account; - } + public int Version { get; set; } = 7; - [Obsolete("for V1 import")] - internal IAccountConfiguration CreateAccount(string server, string accountId) - { - 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; } = ""; - public IAccountConfiguration? FindAccount(string server) - { - return Accounts.FirstOrDefault(a => a.Server == server && a.IsUsable); - } + public DeepDungeonConfiguration DeepDungeons { get; set; } = new(); + public RendererConfiguration Renderer { get; set; } = new(); + public List Accounts { get; set; } = new(); - public void RemoveAccount(string server) - { - Accounts.RemoveAll(a => a.Server == server && a.IsUsable); - } + [JsonIgnore] + [Obsolete] + public List ImportHistory { get; set; } = new(); - public bool HasRoleOnCurrentServer(string role) - { - if (Mode != EMode.Online) - return false; + public IAccountConfiguration CreateAccount(string server, Guid accountId) + { + var account = new AccountConfigurationV7(server, accountId); + Accounts.Add(account); + return account; + } - var account = FindAccount(RemoteApi.RemoteUrl); - return account == null || account.CachedRoles.Contains(role); + [Obsolete("for V1 import")] + internal IAccountConfiguration CreateAccount(string server, string accountId) + { + var account = new AccountConfigurationV7(server, accountId); + Accounts.Add(account); + return account; + } + + 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 role) + { + if (Mode != EMode.Online) + return false; + + var account = FindAccount(RemoteApi.RemoteUrl); + return account == null || account.CachedRoles.Contains(role); + } } } diff --git a/Pal.Client/DependencyInjection/DebugState.cs b/Pal.Client/DependencyInjection/DebugState.cs index 0a632a3..1fe7624 100644 --- a/Pal.Client/DependencyInjection/DebugState.cs +++ b/Pal.Client/DependencyInjection/DebugState.cs @@ -2,7 +2,7 @@ namespace Pal.Client.DependencyInjection { - internal class DebugState + internal sealed class DebugState { public string? DebugMessage { get; set; } diff --git a/Pal.Client/DependencyInjection/DIPlugin.cs b/Pal.Client/DependencyInjection/DependencyInjectionContext.cs similarity index 93% rename from Pal.Client/DependencyInjection/DIPlugin.cs rename to Pal.Client/DependencyInjection/DependencyInjectionContext.cs index e77e056..62241e1 100644 --- a/Pal.Client/DependencyInjection/DIPlugin.cs +++ b/Pal.Client/DependencyInjection/DependencyInjectionContext.cs @@ -22,13 +22,14 @@ namespace Pal.Client.DependencyInjection /// /// DI-aware Plugin. /// - internal sealed class DIPlugin : IDalamudPlugin + // ReSharper disable once UnusedType.Global + internal sealed class DependencyInjectionContext : IDalamudPlugin { private ServiceProvider? _serviceProvider; public string Name => Localization.Palace_Pal; - public DIPlugin(DalamudPluginInterface pluginInterface, + public DependencyInjectionContext(DalamudPluginInterface pluginInterface, ClientState clientState, GameGui gameGui, ChatGui chatGui, @@ -51,7 +52,7 @@ namespace Pal.Client.DependencyInjection services.AddSingleton(condition); services.AddSingleton(commandManager); services.AddSingleton(dataManager); - services.AddSingleton(new WindowSystem(typeof(DIPlugin).AssemblyQualifiedName)); + services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); // plugin-specific services.AddSingleton(); diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs index f3adaee..aeb05df 100644 --- a/Pal.Client/DependencyInjection/FrameworkService.cs +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -18,7 +18,7 @@ using Pal.Client.Scheduled; namespace Pal.Client.DependencyInjection { - internal class FrameworkService : IDisposable + internal sealed class FrameworkService : IDisposable { private readonly Framework _framework; private readonly ConfigurationManager _configurationManager; diff --git a/Pal.Client/Hooks.cs b/Pal.Client/Hooks.cs index 4250075..dbdcb4f 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -10,7 +10,7 @@ using Pal.Client.DependencyInjection; namespace Pal.Client { - internal unsafe class Hooks : IDisposable + internal sealed unsafe class Hooks : IDisposable { private readonly ObjectTable _objectTable; private readonly TerritoryState _territoryState; diff --git a/Pal.Client/LocalState.cs b/Pal.Client/LocalState.cs index 4044768..8713da6 100644 --- a/Pal.Client/LocalState.cs +++ b/Pal.Client/LocalState.cs @@ -12,7 +12,7 @@ namespace Pal.Client /// /// JSON for a single floor set (e.g. 51-60). /// - internal class LocalState + internal sealed class LocalState { private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; private const int CurrentVersion = 4; @@ -146,7 +146,7 @@ namespace Pal.Client marker.Imports.RemoveAll(importIds.Contains); } - public class SaveFile + public sealed class SaveFile { public int Version { get; set; } public HashSet Markers { get; set; } = new(); diff --git a/Pal.Client/Marker.cs b/Pal.Client/Marker.cs index e20d0fe..e2e79b9 100644 --- a/Pal.Client/Marker.cs +++ b/Pal.Client/Marker.cs @@ -9,7 +9,7 @@ using System.Text.Json.Serialization; namespace Pal.Client { - internal class Marker + internal sealed class Marker { public EType Type { get; set; } = EType.Unknown; public Vector3 Position { get; set; } diff --git a/Pal.Client/Net/GrpcLogger.cs b/Pal.Client/Net/GrpcLogger.cs index 9c12149..717063c 100644 --- a/Pal.Client/Net/GrpcLogger.cs +++ b/Pal.Client/Net/GrpcLogger.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; namespace Pal.Client.Net { - internal class GrpcLogger : ILogger + internal sealed class GrpcLogger : ILogger { private readonly string _name; diff --git a/Pal.Client/Net/GrpcLoggerProvider.cs b/Pal.Client/Net/GrpcLoggerProvider.cs index bf702e5..f9db63f 100644 --- a/Pal.Client/Net/GrpcLoggerProvider.cs +++ b/Pal.Client/Net/GrpcLoggerProvider.cs @@ -3,7 +3,7 @@ using System; namespace Pal.Client.Net { - internal class GrpcLoggerProvider : ILoggerProvider + internal sealed class GrpcLoggerProvider : ILoggerProvider { public ILogger CreateLogger(string categoryName) => new GrpcLogger(categoryName); diff --git a/Pal.Client/Net/JwtClaims.cs b/Pal.Client/Net/JwtClaims.cs index 3247bba..5d8704b 100644 --- a/Pal.Client/Net/JwtClaims.cs +++ b/Pal.Client/Net/JwtClaims.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace Pal.Client.Net { - internal class JwtClaims + internal sealed class JwtClaims { [JsonPropertyName("nameid")] public Guid NameId { get; set; } @@ -46,7 +46,7 @@ namespace Pal.Client.Net } } - internal class JwtRoleConverter : JsonConverter> + internal sealed class JwtRoleConverter : JsonConverter> { public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -78,9 +78,9 @@ namespace Pal.Client.Net public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) => throw new NotImplementedException(); } - public class JwtDateConverter : JsonConverter + public sealed class JwtDateConverter : JsonConverter { - static readonly DateTimeOffset Zero = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + static readonly DateTimeOffset Zero = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 8d1449b..2ee2e09 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -1,17 +1,11 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Interface.Windowing; +using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Pal.Client.Rendering; -using Pal.Client.Scheduled; using Pal.Client.Windows; using System; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Runtime.InteropServices; using Dalamud.Logging; -using Pal.Client.Extensions; using Pal.Client.Properties; using ECommons; using Microsoft.Extensions.DependencyInjection; diff --git a/Pal.Client/Windows/StatisticsWindow.cs b/Pal.Client/Windows/StatisticsWindow.cs index 05f953a..90dfe5b 100644 --- a/Pal.Client/Windows/StatisticsWindow.cs +++ b/Pal.Client/Windows/StatisticsWindow.cs @@ -13,7 +13,7 @@ using System.Reflection; namespace Pal.Client.Windows { - internal class StatisticsWindow : Window, IDisposable, ILanguageChanged + internal sealed class StatisticsWindow : Window, IDisposable, ILanguageChanged { private const string WindowId = "###PalacePalStats"; private readonly WindowSystem _windowSystem; @@ -112,7 +112,7 @@ namespace Pal.Client.Windows } } - private class TerritoryStatistics + private sealed class TerritoryStatistics { public string TerritoryName { get; } public uint? TrapCount { get; set; } From 7d04cd757512b6abb045156be9a1a257d6836782 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 10:25:33 +0100 Subject: [PATCH 10/38] DI: Split QueueHandler into multiple classes --- .../DependencyInjectionContext.cs | 7 +- .../DependencyInjection/FrameworkService.cs | 80 +++++-- .../Scheduled/IQueueOnFrameworkThread.cs | 27 ++- Pal.Client/Scheduled/QueueHandler.cs | 203 ------------------ Pal.Client/Scheduled/QueuedConfigUpdate.cs | 33 ++- Pal.Client/Scheduled/QueuedImport.cs | 147 +++++++++---- Pal.Client/Scheduled/QueuedSyncResponse.cs | 91 +++++++- Pal.Client/Scheduled/QueuedUndoImport.cs | 33 ++- 8 files changed, 352 insertions(+), 269 deletions(-) delete mode 100644 Pal.Client/Scheduled/QueueHandler.cs diff --git a/Pal.Client/DependencyInjection/DependencyInjectionContext.cs b/Pal.Client/DependencyInjection/DependencyInjectionContext.cs index 62241e1..d8b5e55 100644 --- a/Pal.Client/DependencyInjection/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjection/DependencyInjectionContext.cs @@ -69,7 +69,6 @@ namespace Pal.Client.DependencyInjection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); // windows & related services services.AddSingleton(); @@ -82,6 +81,12 @@ namespace Pal.Client.DependencyInjection services.AddSingleton(); services.AddSingleton(); + // queue handling + services.AddTransient, QueuedImport.Handler>(); + services.AddTransient, QueuedUndoImport.Handler>(); + services.AddTransient, QueuedConfigUpdate.Handler>(); + services.AddTransient, QueuedSyncResponse.Handler>(); + // set up the current UI language before creating anything Localization.Culture = new CultureInfo(pluginInterface.UiLanguage); diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs index aeb05df..8db9d10 100644 --- a/Pal.Client/DependencyInjection/FrameworkService.cs +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -9,7 +9,9 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; using ImGuiNET; +using Microsoft.Extensions.DependencyInjection; using Pal.Client.Configuration; using Pal.Client.Extensions; using Pal.Client.Net; @@ -20,6 +22,7 @@ namespace Pal.Client.DependencyInjection { internal sealed class FrameworkService : IDisposable { + private readonly IServiceProvider _serviceProvider; private readonly Framework _framework; private readonly ConfigurationManager _configurationManager; private readonly IPalacePalConfiguration _configuration; @@ -28,7 +31,6 @@ namespace Pal.Client.DependencyInjection private readonly FloorService _floorService; private readonly DebugState _debugState; private readonly RenderAdapter _renderAdapter; - private readonly QueueHandler _queueHandler; private readonly ObjectTable _objectTable; private readonly RemoteApi _remoteApi; @@ -36,7 +38,9 @@ namespace Pal.Client.DependencyInjection internal Queue LateEventQueue { get; } = new(); internal ConcurrentQueue NextUpdateObjects { get; } = new(); - public FrameworkService(Framework framework, + public FrameworkService( + IServiceProvider serviceProvider, + Framework framework, ConfigurationManager configurationManager, IPalacePalConfiguration configuration, ClientState clientState, @@ -44,10 +48,10 @@ namespace Pal.Client.DependencyInjection FloorService floorService, DebugState debugState, RenderAdapter renderAdapter, - QueueHandler queueHandler, ObjectTable objectTable, RemoteApi remoteApi) { + _serviceProvider = serviceProvider; _framework = framework; _configurationManager = configurationManager; _configuration = configuration; @@ -56,7 +60,6 @@ namespace Pal.Client.DependencyInjection _floorService = floorService; _debugState = debugState; _renderAdapter = renderAdapter; - _queueHandler = queueHandler; _objectTable = objectTable; _remoteApi = remoteApi; @@ -84,7 +87,7 @@ namespace Pal.Client.DependencyInjection bool saveMarkers = false; while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - _queueHandler.Handle(queued, ref recreateLayout, ref saveMarkers); + HandleQueued(queued, ref recreateLayout, ref saveMarkers); if (_territoryState.LastTerritory != _clientState.TerritoryType) { @@ -111,12 +114,13 @@ namespace Pal.Client.DependencyInjection } while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - _queueHandler.Handle(queued, ref recreateLayout, ref saveMarkers); + HandleQueued(queued, ref recreateLayout, ref saveMarkers); var currentFloor = _floorService.GetFloorMarkers(_territoryState.LastTerritory); IList visibleMarkers = GetRelevantGameObjects(); - HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout); + HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, + recreateLayout); HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); } catch (Exception e) @@ -126,7 +130,9 @@ namespace Pal.Client.DependencyInjection } #region Render Markers - private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, bool recreateLayout) + + private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, + bool recreateLayout) { var currentFloorMarkers = currentFloor.Markers; @@ -145,7 +151,8 @@ namespace Pal.Client.DependencyInjection // 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 && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && !knownMarker.RemoteSeenOn.Contains(partialAccountId)) + if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && + !knownMarker.RemoteSeenOn.Contains(partialAccountId)) updateSeenMarkers = true; continue; @@ -156,9 +163,10 @@ namespace Pal.Client.DependencyInjection saveMarkers = true; } - if (!recreateLayout && currentFloorMarkers.Count > 0 && (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) + if (!recreateLayout && currentFloorMarkers.Count > 0 && + (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || + _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) { - try { foreach (var marker in currentFloorMarkers) @@ -183,7 +191,9 @@ namespace Pal.Client.DependencyInjection if (updateSeenMarkers && partialAccountId != null) { - var markersToUpdate = currentFloorMarkers.Where(x => x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && !x.RemoteSeenOn.Contains(partialAccountId)).ToList(); + var markersToUpdate = currentFloorMarkers.Where(x => + x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && + !x.RemoteSeenOn.Contains(partialAccountId)).ToList(); foreach (var marker in markersToUpdate) marker.RemoteSeenRequested = true; Task.Run(async () => await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, markersToUpdate)); @@ -195,12 +205,14 @@ namespace Pal.Client.DependencyInjection if (_territoryState.TerritorySyncState == SyncState.Complete) { - var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); + var markersToUpload = currentFloorMarkers + .Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); if (markersToUpload.Count > 0) { foreach (var marker in markersToUpload) marker.UploadRequested = true; - Task.Run(async () => await UploadMarkersForTerritory(_territoryState.LastTerritory, markersToUpload)); + Task.Run(async () => + await UploadMarkersForTerritory(_territoryState.LastTerritory, markersToUpload)); } } } @@ -212,15 +224,18 @@ namespace Pal.Client.DependencyInjection List elements = new(); foreach (var marker in currentFloorMarkers) { - if (marker.Seen || _configuration.Mode == EMode.Online || marker is { WasImported: true, Imports.Count: > 0 }) + if (marker.Seen || _configuration.Mode == EMode.Online || + marker is { WasImported: true, Imports.Count: > 0 }) { if (marker.Type == Marker.EType.Trap) { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.Traps); + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), + _configuration.DeepDungeons.Traps); } else if (marker.Type == Marker.EType.Hoard) { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.HoardCoffers); + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), + _configuration.DeepDungeons.HoardCoffers); } } } @@ -234,8 +249,10 @@ namespace Pal.Client.DependencyInjection private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) { - recreateLayout |= _floorService.EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); - recreateLayout |= visibleMarkers.Any(visibleMarker => _floorService.EphemeralMarkers.All(x => x != visibleMarker)); + recreateLayout |= + _floorService.EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); + recreateLayout |= + visibleMarkers.Any(visibleMarker => _floorService.EphemeralMarkers.All(x => x != visibleMarker)); if (recreateLayout) { @@ -249,7 +266,8 @@ namespace Pal.Client.DependencyInjection if (marker.Type == Marker.EType.SilverCoffer && _configuration.DeepDungeons.SilverCoffers.Show) { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.SilverCoffers); + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), + _configuration.DeepDungeons.SilverCoffers); } } @@ -264,9 +282,13 @@ namespace Pal.Client.DependencyInjection { switch (marker.Type) { - case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive || !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive || + !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || + visibleMarkers.Any(x => x == marker): return _configuration.DeepDungeons.Traps.Color; - case Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive || !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + case Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive || + !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || + visibleMarkers.Any(x => x == marker): return _configuration.DeepDungeons.HoardCoffers.Color; case Marker.EType.SilverCoffer: return _configuration.DeepDungeons.SilverCoffers.Color; @@ -278,7 +300,8 @@ namespace Pal.Client.DependencyInjection } } - private void CreateRenderElement(Marker marker, List elements, uint color, MarkerConfiguration config) + private void CreateRenderElement(Marker marker, List elements, uint color, + MarkerConfiguration config) { if (!config.Show) return; @@ -287,9 +310,11 @@ namespace Pal.Client.DependencyInjection marker.RenderElement = element; elements.Add(element); } + #endregion #region Up-/Download + private async Task DownloadMarkersForTerritory(ushort territoryId) { try @@ -346,6 +371,7 @@ namespace Pal.Client.DependencyInjection _debugState.SetFromException(e); } } + #endregion private IList GetRelevantGameObjects() @@ -388,5 +414,13 @@ namespace Pal.Client.DependencyInjection return result; } + + private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) + { + Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType()); + var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType); + + handler.RunIfCompatible(queued, ref recreateLayout, ref saveMarkers); + } } } diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index d04446f..a8d4aeb 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -1,6 +1,31 @@ -namespace Pal.Client.Scheduled +using Dalamud.Logging; + +namespace Pal.Client.Scheduled { internal interface IQueueOnFrameworkThread { + internal interface IHandler + { + void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers); + } + + internal abstract class Handler : IHandler + where T : IQueueOnFrameworkThread + { + protected abstract void Run(T queued, ref bool recreateLayout, ref bool saveMarkers); + + public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) + { + if (queued is T t) + { + PluginLog.Information($"Handling {queued.GetType()} with handler {GetType()}"); + Run(t, ref recreateLayout, ref saveMarkers); + } + else + { + PluginLog.Error($"Could not use queue handler {GetType()} with type {queued.GetType()}"); + } + } + } } } diff --git a/Pal.Client/Scheduled/QueueHandler.cs b/Pal.Client/Scheduled/QueueHandler.cs deleted file mode 100644 index 8ec67ef..0000000 --- a/Pal.Client/Scheduled/QueueHandler.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.Gui; -using Dalamud.Logging; -using Pal.Client.Configuration; -using Pal.Client.DependencyInjection; -using Pal.Client.Extensions; -using Pal.Client.Net; -using Pal.Client.Properties; -using Pal.Common; - -namespace Pal.Client.Scheduled -{ - // TODO The idea was to split this from the queue objects, should be in individual classes tho - internal sealed class QueueHandler - { - private readonly ConfigurationManager _configurationManager; - private readonly IPalacePalConfiguration _configuration; - private readonly FloorService _floorService; - private readonly TerritoryState _territoryState; - private readonly DebugState _debugState; - private readonly ChatGui _chatGui; - - public QueueHandler( - ConfigurationManager configurationManager, - IPalacePalConfiguration configuration, - FloorService floorService, - TerritoryState territoryState, - DebugState debugState, - ChatGui chatGui) - { - _configurationManager = configurationManager; - _configuration = configuration; - _floorService = floorService; - _territoryState = territoryState; - _debugState = debugState; - _chatGui = chatGui; - } - - public void Handle(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) - { - if (queued is QueuedConfigUpdate) - { - ConfigUpdate(ref recreateLayout, ref saveMarkers); - } - else if (queued is QueuedSyncResponse queuedSyncResponse) - { - SyncResponse(queuedSyncResponse); - recreateLayout = true; - saveMarkers = true; - } - else if (queued is QueuedImport queuedImport) - { - Import(queuedImport); - recreateLayout = true; - saveMarkers = true; - } - else if (queued is QueuedUndoImport queuedUndoImport) - { - UndoImport(queuedUndoImport); - recreateLayout = true; - saveMarkers = true; - } - else - throw new InvalidOperationException(); - } - - private void ConfigUpdate(ref bool recreateLayout, ref bool saveMarkers) - { - if (_configuration.Mode == EMode.Offline) - { - LocalState.UpdateAll(); - _floorService.FloorMarkers.Clear(); - _floorService.EphemeralMarkers.Clear(); - _territoryState.LastTerritory = 0; - - recreateLayout = true; - saveMarkers = true; - } - } - - private void SyncResponse(QueuedSyncResponse queued) - { - try - { - var remoteMarkers = queued.Markers; - var currentFloor = _floorService.GetFloorMarkers(queued.TerritoryType); - if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) - { - switch (queued.Type) - { - case SyncType.Download: - case SyncType.Upload: - foreach (var remoteMarker in remoteMarkers) - { - // Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved. - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - { - localMarker.NetworkId = remoteMarker.NetworkId; - continue; - } - - if (queued.Type == SyncType.Download) - currentFloor.Markers.Add(remoteMarker); - } - - break; - - case SyncType.MarkSeen: - var partialAccountId = - _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - if (partialAccountId == null) - break; - foreach (var remoteMarker in remoteMarkers) - { - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - localMarker.RemoteSeenOn.Add(partialAccountId); - } - - break; - } - } - - // don't modify state for outdated floors - if (_territoryState.LastTerritory != queued.TerritoryType) - return; - - if (queued.Type == SyncType.Download) - { - if (queued.Success) - _territoryState.TerritorySyncState = SyncState.Complete; - else - _territoryState.TerritorySyncState = SyncState.Failed; - } - } - catch (Exception e) - { - _debugState.SetFromException(e); - if (queued.Type == SyncType.Download) - _territoryState.TerritorySyncState = SyncState.Failed; - } - } - - private void Import(QueuedImport queued) - { - try - { - if (!queued.Validate(_chatGui)) - return; - - var oldExportIds = string.IsNullOrEmpty(queued.Export.ServerUrl) - ? _configuration.ImportHistory.Where(x => x.RemoteUrl == queued.Export.ServerUrl).Select(x => x.Id) - .Where(x => x != Guid.Empty).ToList() - : new List(); - - foreach (var remoteFloor in queued.Export.Floors) - { - ushort territoryType = (ushort)remoteFloor.TerritoryType; - var localState = _floorService.GetFloorMarkers(territoryType); - - localState.UndoImport(oldExportIds); - queued.ImportFloor(remoteFloor, localState); - - localState.Save(); - } - - _configuration.ImportHistory.RemoveAll(hist => - oldExportIds.Contains(hist.Id) || hist.Id == queued.ExportId); - _configuration.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry - { - Id = queued.ExportId, - RemoteUrl = queued.Export.ServerUrl, - ExportedAt = queued.Export.CreatedAt.ToDateTime(), - ImportedAt = DateTime.UtcNow, - }); - _configurationManager.Save(_configuration); - - _chatGui.Print(string.Format(Localization.ImportCompleteStatistics, queued.ImportedTraps, - queued.ImportedHoardCoffers)); - } - catch (Exception e) - { - PluginLog.Error(e, "Import failed"); - _chatGui.PalError(string.Format(Localization.Error_ImportFailed, e)); - } - } - - private void UndoImport(QueuedUndoImport queued) - { - foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) - { - var localState = _floorService.GetFloorMarkers((ushort)territoryType); - localState.UndoImport(new List { queued.ExportId }); - localState.Save(); - } - - _configuration.ImportHistory.RemoveAll(hist => hist.Id == queued.ExportId); - } - } -} diff --git a/Pal.Client/Scheduled/QueuedConfigUpdate.cs b/Pal.Client/Scheduled/QueuedConfigUpdate.cs index 0536362..67c04a1 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -1,6 +1,37 @@ -namespace Pal.Client.Scheduled +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; + +namespace Pal.Client.Scheduled { internal sealed class QueuedConfigUpdate : IQueueOnFrameworkThread { + internal sealed class Handler : IQueueOnFrameworkThread.Handler + { + private readonly IPalacePalConfiguration _configuration; + private readonly FloorService _floorService; + private readonly TerritoryState _territoryState; + + public Handler(IPalacePalConfiguration configuration, FloorService floorService, + TerritoryState territoryState) + { + _configuration = configuration; + _floorService = floorService; + _territoryState = territoryState; + } + + protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout, ref bool saveMarkers) + { + if (_configuration.Mode == EMode.Offline) + { + LocalState.UpdateAll(); + _floorService.FloorMarkers.Clear(); + _floorService.EphemeralMarkers.Clear(); + _territoryState.LastTerritory = 0; + + recreateLayout = true; + saveMarkers = true; + } + } + } } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 4a74c79..d1e8dda 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -1,20 +1,25 @@ using Account; using Pal.Common; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using Dalamud.Game.Gui; +using Dalamud.Logging; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; using Pal.Client.Properties; namespace Pal.Client.Scheduled { internal sealed class QueuedImport : IQueueOnFrameworkThread { - public ExportRoot Export { get; } - public Guid ExportId { get; private set; } - public int ImportedTraps { get; private set; } - public int ImportedHoardCoffers { get; private set; } + private ExportRoot Export { get; } + private Guid ExportId { get; set; } + private int ImportedTraps { get; set; } + private int ImportedHoardCoffers { get; set; } public QueuedImport(string sourcePath) { @@ -22,50 +27,116 @@ namespace Pal.Client.Scheduled Export = ExportRoot.Parser.ParseFrom(input); } - public bool Validate(ChatGui chatGui) + internal sealed class Handler : IQueueOnFrameworkThread.Handler { - if (Export.ExportVersion != ExportConfig.ExportVersion) + private readonly ChatGui _chatGui; + private readonly IPalacePalConfiguration _configuration; + private readonly ConfigurationManager _configurationManager; + private readonly FloorService _floorService; + + public Handler(ChatGui chatGui, IPalacePalConfiguration configuration, + ConfigurationManager configurationManager, FloorService floorService) { - chatGui.PrintError(Localization.Error_ImportFailed_IncompatibleVersion); - return false; + _chatGui = chatGui; + _configuration = configuration; + _configurationManager = configurationManager; + _floorService = floorService; } - if (!Guid.TryParse(Export.ExportId, out Guid exportId) || ExportId == Guid.Empty) + protected override void Run(QueuedImport import, ref bool recreateLayout, ref bool saveMarkers) { - chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); - return false; - } + recreateLayout = true; + saveMarkers = true; - ExportId = exportId; - - if (string.IsNullOrEmpty(Export.ServerUrl)) - { - // If we allow for backups as import/export, this should be removed - chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); - return false; - } - - return true; - } - - public void ImportFloor(ExportFloor remoteFloor, LocalState localState) - { - var remoteMarkers = remoteFloor.Objects.Select(m => new Marker((Marker.EType)m.Type, new Vector3(m.X, m.Y, m.Z)) { WasImported = true }); - foreach (var remoteMarker in remoteMarkers) - { - Marker? localMarker = localState.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker == null) + try { - localState.Markers.Add(remoteMarker); - localMarker = remoteMarker; + if (!Validate(import)) + return; - if (localMarker.Type == Marker.EType.Trap) - ImportedTraps++; - else if (localMarker.Type == Marker.EType.Hoard) - ImportedHoardCoffers++; + var oldExportIds = string.IsNullOrEmpty(import.Export.ServerUrl) + ? _configuration.ImportHistory.Where(x => x.RemoteUrl == import.Export.ServerUrl) + .Select(x => x.Id) + .Where(x => x != Guid.Empty).ToList() + : new List(); + + foreach (var remoteFloor in import.Export.Floors) + { + ushort territoryType = (ushort)remoteFloor.TerritoryType; + var localState = _floorService.GetFloorMarkers(territoryType); + + localState.UndoImport(oldExportIds); + ImportFloor(import, remoteFloor, localState); + + localState.Save(); + } + + _configuration.ImportHistory.RemoveAll(hist => + oldExportIds.Contains(hist.Id) || hist.Id == import.ExportId); + _configuration.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry + { + Id = import.ExportId, + RemoteUrl = import.Export.ServerUrl, + ExportedAt = import.Export.CreatedAt.ToDateTime(), + ImportedAt = DateTime.UtcNow, + }); + _configurationManager.Save(_configuration); + + _chatGui.Print(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, + import.ImportedHoardCoffers)); + } + catch (Exception e) + { + PluginLog.Error(e, "Import failed"); + _chatGui.PalError(string.Format(Localization.Error_ImportFailed, e)); + } + } + + private bool Validate(QueuedImport import) + { + if (import.Export.ExportVersion != ExportConfig.ExportVersion) + { + _chatGui.PrintError(Localization.Error_ImportFailed_IncompatibleVersion); + return false; } - remoteMarker.Imports.Add(ExportId); + if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || import.ExportId == Guid.Empty) + { + _chatGui.PrintError(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 + _chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); + return false; + } + + return true; + } + + private void ImportFloor(QueuedImport import, ExportFloor remoteFloor, LocalState localState) + { + var remoteMarkers = remoteFloor.Objects.Select(m => + new Marker((Marker.EType)m.Type, new Vector3(m.X, m.Y, m.Z)) { WasImported = true }); + foreach (var remoteMarker in remoteMarkers) + { + Marker? localMarker = localState.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker == null) + { + localState.Markers.Add(remoteMarker); + localMarker = remoteMarker; + + if (localMarker.Type == Marker.EType.Trap) + import.ImportedTraps++; + else if (localMarker.Type == Marker.EType.Hoard) + import.ImportedHoardCoffers++; + } + + remoteMarker.Imports.Add(import.ExportId); + } } } } diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index db94177..442cb87 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -1,4 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; +using Pal.Client.Net; namespace Pal.Client.Scheduled { @@ -8,6 +14,89 @@ namespace Pal.Client.Scheduled public required ushort TerritoryType { get; init; } public required bool Success { get; init; } public required List Markers { get; init; } + + internal sealed class Handler : IQueueOnFrameworkThread.Handler + { + private readonly IPalacePalConfiguration _configuration; + private readonly FloorService _floorService; + private readonly TerritoryState _territoryState; + private readonly DebugState _debugState; + + public Handler(IPalacePalConfiguration configuration, FloorService floorService, TerritoryState territoryState, DebugState debugState) + { + _configuration = configuration; + _floorService = floorService; + _territoryState = territoryState; + _debugState = debugState; + } + + protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout, ref bool saveMarkers) + { + recreateLayout = true; + saveMarkers = true; + + try + { + var remoteMarkers = queued.Markers; + var currentFloor = _floorService.GetFloorMarkers(queued.TerritoryType); + if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) + { + switch (queued.Type) + { + case SyncType.Download: + case SyncType.Upload: + foreach (var remoteMarker in remoteMarkers) + { + // Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved. + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + { + localMarker.NetworkId = remoteMarker.NetworkId; + continue; + } + + if (queued.Type == SyncType.Download) + currentFloor.Markers.Add(remoteMarker); + } + + break; + + case SyncType.MarkSeen: + var partialAccountId = + _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); + if (partialAccountId == null) + break; + foreach (var remoteMarker in remoteMarkers) + { + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + localMarker.RemoteSeenOn.Add(partialAccountId); + } + + break; + } + } + + // don't modify state for outdated floors + if (_territoryState.LastTerritory != queued.TerritoryType) + return; + + if (queued.Type == SyncType.Download) + { + if (queued.Success) + _territoryState.TerritorySyncState = SyncState.Complete; + else + _territoryState.TerritorySyncState = SyncState.Failed; + } + } + catch (Exception e) + { + _debugState.SetFromException(e); + if (queued.Type == SyncType.Download) + _territoryState.TerritorySyncState = SyncState.Failed; + } + } + } } public enum SyncState diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index 961532a..37eb4c2 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; +using Pal.Common; namespace Pal.Client.Scheduled { @@ -9,6 +13,33 @@ namespace Pal.Client.Scheduled ExportId = exportId; } - public Guid ExportId { get; } + private Guid ExportId { get; } + + internal sealed class Handler : IQueueOnFrameworkThread.Handler + { + private readonly IPalacePalConfiguration _configuration; + private readonly FloorService _floorService; + + public Handler(IPalacePalConfiguration configuration, FloorService floorService) + { + _configuration = configuration; + _floorService = floorService; + } + + protected override void Run(QueuedUndoImport queued, ref bool recreateLayout, ref bool saveMarkers) + { + recreateLayout = true; + saveMarkers = true; + + foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) + { + var localState = _floorService.GetFloorMarkers((ushort)territoryType); + localState.UndoImport(new List { queued.ExportId }); + localState.Save(); + } + + _configuration.ImportHistory.RemoveAll(hist => hist.Id == queued.ExportId); + } + } } } From e3459a01828024dd276fbb78e57f410740f7e0ea Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 10:29:17 +0100 Subject: [PATCH 11/38] DI: Remove WindowSystemExtensions --- Pal.Client/Extensions/WindowSystemExtensions.cs | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 Pal.Client/Extensions/WindowSystemExtensions.cs diff --git a/Pal.Client/Extensions/WindowSystemExtensions.cs b/Pal.Client/Extensions/WindowSystemExtensions.cs deleted file mode 100644 index 15809c2..0000000 --- a/Pal.Client/Extensions/WindowSystemExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Linq; -using Dalamud.Interface.Windowing; - -namespace Pal.Client.Extensions -{ - internal static class WindowSystemExtensions - { - public static T? GetWindow(this WindowSystem windowSystem) - where T : Window - { - return windowSystem.Windows.OfType().FirstOrDefault(); - } - } -} From 3d560fad7f4b1b74898402bc3d71d8f7db66b4c6 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 10:46:19 +0100 Subject: [PATCH 12/38] Make chat messages/errors consistent --- Pal.Client/Commands/PalCommand.cs | 4 ++-- .../DependencyInjection/StatisticsService.cs | 2 +- Pal.Client/Extensions/ChatExtensions.cs | 19 ++++++++++++++++++- Pal.Client/Hooks.cs | 2 +- Pal.Client/Rendering/SplatoonRenderer.cs | 16 +++++++++------- Pal.Client/Scheduled/QueuedImport.cs | 8 ++++---- Pal.Client/Windows/ConfigWindow.cs | 13 ++++++++----- 7 files changed, 43 insertions(+), 21 deletions(-) diff --git a/Pal.Client/Commands/PalCommand.cs b/Pal.Client/Commands/PalCommand.cs index 4d9ff97..336623f 100644 --- a/Pal.Client/Commands/PalCommand.cs +++ b/Pal.Client/Commands/PalCommand.cs @@ -82,7 +82,7 @@ namespace Pal.Client.Commands #if DEBUG case "update-saves": LocalState.UpdateAll(); - Service.Chat.Print(Localization.Command_pal_updatesaves); + _chatGui.PalMessage(Localization.Command_pal_updatesaves); break; #endif @@ -124,7 +124,7 @@ namespace Pal.Client.Commands var playerPosition = _clientState.LocalPlayer?.Position; if (playerPosition == null) return; - _chatGui.Print($"[Palace Pal] {playerPosition}"); + _chatGui.PalMessage($"{playerPosition}"); var nearbyMarkers = state.Markers .Where(m => predicate(m)) diff --git a/Pal.Client/DependencyInjection/StatisticsService.cs b/Pal.Client/DependencyInjection/StatisticsService.cs index 23f2ba9..bdbbaac 100644 --- a/Pal.Client/DependencyInjection/StatisticsService.cs +++ b/Pal.Client/DependencyInjection/StatisticsService.cs @@ -54,7 +54,7 @@ namespace Pal.Client.DependencyInjection } catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) { - _chatGui.Print(Localization.Command_pal_stats_CurrentFloor); + _chatGui.PalError(Localization.Command_pal_stats_CurrentFloor); } catch (Exception e) { diff --git a/Pal.Client/Extensions/ChatExtensions.cs b/Pal.Client/Extensions/ChatExtensions.cs index 31b39ef..ff7cac4 100644 --- a/Pal.Client/Extensions/ChatExtensions.cs +++ b/Pal.Client/Extensions/ChatExtensions.cs @@ -1,4 +1,6 @@ using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; using Pal.Client.Properties; namespace Pal.Client.Extensions @@ -6,6 +8,21 @@ namespace Pal.Client.Extensions public static class ChatExtensions { public static void PalError(this ChatGui chat, string e) - => chat.PrintError($"[{Localization.Palace_Pal}] {e}"); + { + chat.PrintChat(new XivChatEntry + { + Message = new SeStringBuilder() + .AddUiForeground($"[{Localization.Palace_Pal}] ", 16) + .AddText(e).Build(), + Type = XivChatType.Urgent + }); + } + + public static void PalMessage(this ChatGui chat, string message) + { + chat.Print(new SeStringBuilder() + .AddUiForeground($"[{Localization.Palace_Pal}] ", 57) + .AddText(message).Build()); + } } } diff --git a/Pal.Client/Hooks.cs b/Pal.Client/Hooks.cs index dbdcb4f..24aa439 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -72,7 +72,7 @@ namespace Pal.Client /* if (Service.Configuration.BetaKey == "VFX") - Service.Chat.Print($"{vfxPath} on {obj}"); + _chatGui.PalPrint($"{vfxPath} on {obj}"); */ if (obj is BattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395)) diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 2fc5c02..4ebd659 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -14,6 +14,7 @@ using System.Reflection; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; namespace Pal.Client.Rendering { @@ -25,7 +26,8 @@ namespace Pal.Client.Rendering private readonly ClientState _clientState; private readonly ChatGui _chatGui; - public SplatoonRenderer(DalamudPluginInterface pluginInterface, IDalamudPlugin dalamudPlugin, DebugState debugState, + public SplatoonRenderer(DalamudPluginInterface pluginInterface, IDalamudPlugin dalamudPlugin, + DebugState debugState, ClientState clientState, ChatGui chatGui) { _debugState = debugState; @@ -109,7 +111,7 @@ namespace Pal.Client.Rendering elements.Cast().Select(x => x.Delegate).ToArray(), new[] { Environment.TickCount64 + 10000 })) { - _chatGui.PrintError("Could not draw markers :("); + _chatGui.PalMessage("Could not draw markers :("); } } } @@ -129,10 +131,10 @@ namespace Pal.Client.Rendering string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t); if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon") { - _chatGui.PrintError( - $"[Palace Pal] Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); - _chatGui.Print( - "[Palace Pal] You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); + _chatGui.PalError( + $"Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); + _chatGui.PalMessage( + "You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); return; } } @@ -142,7 +144,7 @@ namespace Pal.Client.Rendering // not relevant } - _chatGui.PrintError("Could not draw markers, is Splatoon installed and enabled?"); + _chatGui.PalError("Could not draw markers, is Splatoon installed and enabled?"); } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index d1e8dda..fe263b1 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -81,7 +81,7 @@ namespace Pal.Client.Scheduled }); _configurationManager.Save(_configuration); - _chatGui.Print(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, + _chatGui.PalMessage(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, import.ImportedHoardCoffers)); } catch (Exception e) @@ -95,13 +95,13 @@ namespace Pal.Client.Scheduled { if (import.Export.ExportVersion != ExportConfig.ExportVersion) { - _chatGui.PrintError(Localization.Error_ImportFailed_IncompatibleVersion); + _chatGui.PalError(Localization.Error_ImportFailed_IncompatibleVersion); return false; } if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || import.ExportId == Guid.Empty) { - _chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); + _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); return false; } @@ -110,7 +110,7 @@ namespace Pal.Client.Scheduled if (string.IsNullOrEmpty(import.Export.ServerUrl)) { // If we allow for backups as import/export, this should be removed - _chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); + _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); return false; } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index e9fb03e..fd2118e 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -22,6 +22,7 @@ using Dalamud.Game.Gui; using Pal.Client.Properties; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; namespace Pal.Client.Windows { @@ -351,7 +352,8 @@ namespace Pal.Client.Windows ImGui.Text(Localization.Config_Splatoon_Test); ImGui.BeginDisabled(!(_renderAdapter.Implementation is IDrawDebugItems)); if (ImGui.Button(Localization.Config_Splatoon_DrawCircles)) - (_renderAdapter.Implementation as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, _hoardConfig.Color); + (_renderAdapter.Implementation as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, + _hoardConfig.Color); ImGui.EndDisabled(); ImGui.EndTabItem(); @@ -385,7 +387,8 @@ namespace Pal.Client.Windows if (_silverConfig.Show) { - int silverCoffers = _floorService.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); + int silverCoffers = + _floorService.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); ImGui.Text( $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); } @@ -475,17 +478,17 @@ namespace Pal.Client.Windows await using var output = File.Create(destinationPath); export.WriteTo(output); - _chatGui.Print($"Export saved as {destinationPath}."); + _chatGui.PalMessage($"Export saved as {destinationPath}."); } else { - _chatGui.PrintError("Export failed due to server error."); + _chatGui.PalError("Export failed due to server error."); } } catch (Exception e) { PluginLog.Error(e, "Export failed"); - _chatGui.PrintError($"Export failed: {e}"); + _chatGui.PalError($"Export failed: {e}"); } }); } From e27f5a3201eb232063b24f09f76ab83095c85374 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 10:51:25 +0100 Subject: [PATCH 13/38] Export: Version bump --- Pal.Client/Scheduled/QueuedImport.cs | 4 ++++ Pal.Common/ExportConfig.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index fe263b1..ce13b30 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -95,12 +95,15 @@ namespace Pal.Client.Scheduled { if (import.Export.ExportVersion != ExportConfig.ExportVersion) { + PluginLog.Error( + $"Import: Different version in export file, {import.Export.ExportVersion} != {ExportConfig.ExportVersion}"); _chatGui.PalError(Localization.Error_ImportFailed_IncompatibleVersion); return false; } if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || import.ExportId == Guid.Empty) { + PluginLog.Error("Import: Invalid export id"); _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); return false; } @@ -110,6 +113,7 @@ namespace Pal.Client.Scheduled if (string.IsNullOrEmpty(import.Export.ServerUrl)) { // If we allow for backups as import/export, this should be removed + PluginLog.Error("Import: No server URL"); _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); return false; } diff --git a/Pal.Common/ExportConfig.cs b/Pal.Common/ExportConfig.cs index e664ee2..408b6f2 100644 --- a/Pal.Common/ExportConfig.cs +++ b/Pal.Common/ExportConfig.cs @@ -8,6 +8,6 @@ namespace Pal.Common { public static class ExportConfig { - public static int ExportVersion => 1; + public static int ExportVersion => 2; } } From 5c8238216156abb2402931b51c22b4725b45027c Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 13:17:55 +0100 Subject: [PATCH 14/38] DI: Remove Service class --- .../Configuration/ConfigurationManager.cs | 3 ++- .../DependencyInjectionContext.cs | 11 ++++++++--- Pal.Client/LocalState.cs | 9 +++++++-- Pal.Client/Plugin.cs | 18 ++++++++---------- Pal.Client/Rendering/RenderAdapter.cs | 6 ++++++ Pal.Client/Service.cs | 15 --------------- 6 files changed, 31 insertions(+), 31 deletions(-) delete mode 100644 Pal.Client/Service.cs diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index ac2b808..2e8e284 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -39,6 +39,7 @@ namespace Pal.Client.Configuration new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), Encoding.UTF8); + if (queue && config is ConfigurationV7 v7) Saved?.Invoke(this, v7); } @@ -60,7 +61,7 @@ namespace Pal.Client.Configuration var v7 = MigrateToV7(configurationV1); Save(v7, queue: false); - //File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); + File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); } } diff --git a/Pal.Client/DependencyInjection/DependencyInjectionContext.cs b/Pal.Client/DependencyInjection/DependencyInjectionContext.cs index d8b5e55..6d2812f 100644 --- a/Pal.Client/DependencyInjection/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjection/DependencyInjectionContext.cs @@ -108,10 +108,15 @@ namespace Pal.Client.DependencyInjection _serviceProvider.GetService(); #endif - _serviceProvider.GetRequiredService(); + // set up legacy services + LocalState.PluginInterface = pluginInterface; + LocalState.Mode = _serviceProvider.GetRequiredService().Mode; + + // windows that have logic to open on startup _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); + + // initialize components that are mostly self-contained/self-registered + _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); diff --git a/Pal.Client/LocalState.cs b/Pal.Client/LocalState.cs index 8713da6..41a125d 100644 --- a/Pal.Client/LocalState.cs +++ b/Pal.Client/LocalState.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; +using Dalamud.Plugin; +using Pal.Client.Configuration; using Pal.Client.Extensions; namespace Pal.Client @@ -17,6 +19,9 @@ namespace Pal.Client private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; private const int CurrentVersion = 4; + internal static DalamudPluginInterface PluginInterface { get; set; } + internal static EMode Mode { get; set; } + public uint TerritoryType { get; set; } public ConcurrentBag Markers { get; set; } = new(); @@ -27,7 +32,7 @@ namespace Pal.Client private void ApplyFilters() { - if (Service.Configuration.Mode == Configuration.EMode.Offline) + if (Mode == EMode.Offline) Markers = new ConcurrentBag(Markers.Where(x => x.Seen || (x.WasImported && x.Imports.Count > 0))); else // ensure old import markers are removed if they are no longer part of a "current" import @@ -121,7 +126,7 @@ namespace Pal.Client public string GetSaveLocation() => GetSaveLocation(TerritoryType); - private static string GetSaveLocation(uint territoryType) => Path.Join(Service.PluginInterface.GetPluginConfigDirectory(), $"{territoryType}.json"); + private static string GetSaveLocation(uint territoryType) => Path.Join(PluginInterface.GetPluginConfigDirectory(), $"{territoryType}.json"); public static void ForEach(Action action) { diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 2ee2e09..ddd2399 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -13,6 +13,11 @@ using Pal.Client.Configuration; 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 : IDisposable { private readonly IServiceProvider _serviceProvider; @@ -33,10 +38,6 @@ namespace Pal.Client _configuration = configuration; _renderAdapter = renderAdapter; - // initialize legacy services - pluginInterface.Create(); - Service.Configuration = configuration; - LanguageChanged(pluginInterface.UiLanguage); pluginInterface.UiBuilder.Draw += Draw; @@ -55,26 +56,23 @@ namespace Pal.Client configWindow.IsOpen = true; } - #region IDisposable Support public void Dispose() { _pluginInterface.UiBuilder.Draw -= Draw; _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; _pluginInterface.LanguageChanged -= LanguageChanged; } - #endregion private void LanguageChanged(string languageCode) { Localization.Culture = new CultureInfo(languageCode); - _serviceProvider.GetRequiredService().Windows.OfType().Each(w => w.LanguageChanged()); + _serviceProvider.GetRequiredService().Windows.OfType() + .Each(w => w.LanguageChanged()); } private void Draw() { - if (_renderAdapter.Implementation is SimpleRenderer sr) - sr.DrawLayers(); - + _renderAdapter.DrawLayers(); _serviceProvider.GetRequiredService().Draw(); } } diff --git a/Pal.Client/Rendering/RenderAdapter.cs b/Pal.Client/Rendering/RenderAdapter.cs index da8a28f..4afca66 100644 --- a/Pal.Client/Rendering/RenderAdapter.cs +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -29,5 +29,11 @@ namespace Pal.Client.Rendering public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) => Implementation.CreateElement(type, pos, color, fill); + + public void DrawLayers() + { + if (Implementation is SimpleRenderer sr) + sr.DrawLayers(); + } } } diff --git a/Pal.Client/Service.cs b/Pal.Client/Service.cs deleted file mode 100644 index b5d0dc5..0000000 --- a/Pal.Client/Service.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Dalamud.IoC; -using Dalamud.Plugin; -using Pal.Client.Configuration; - -namespace Pal.Client -{ - [Obsolete] - public class Service - { - [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - - internal static IPalacePalConfiguration Configuration { get; set; } = null!; - } -} From 7e2ccd3b420e401c0051a63dada13f5e0e3d41b1 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 13:27:28 +0100 Subject: [PATCH 15/38] Config: Remove Pal.Client.Net dependency for HasRole check --- Pal.Client/Configuration/ConfigurationV7.cs | 5 ++--- Pal.Client/Configuration/IPalacePalConfiguration.cs | 2 +- Pal.Client/DependencyInjection/StatisticsService.cs | 2 +- Pal.Client/Windows/ConfigWindow.cs | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs index 9c2012f..a6032d6 100644 --- a/Pal.Client/Configuration/ConfigurationV7.cs +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -using Pal.Client.Net; namespace Pal.Client.Configuration { @@ -47,12 +46,12 @@ namespace Pal.Client.Configuration Accounts.RemoveAll(a => a.Server == server && a.IsUsable); } - public bool HasRoleOnCurrentServer(string role) + public bool HasRoleOnCurrentServer(string server, string role) { if (Mode != EMode.Online) return false; - var account = FindAccount(RemoteApi.RemoteUrl); + var account = FindAccount(server); return account == null || account.CachedRoles.Contains(role); } } diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index 1c6a747..2752fce 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -30,7 +30,7 @@ namespace Pal.Client.Configuration IAccountConfiguration? FindAccount(string server); void RemoveAccount(string server); - bool HasRoleOnCurrentServer(string role); + bool HasRoleOnCurrentServer(string server, string role); } public class DeepDungeonConfiguration diff --git a/Pal.Client/DependencyInjection/StatisticsService.cs b/Pal.Client/DependencyInjection/StatisticsService.cs index bdbbaac..b7ee3cf 100644 --- a/Pal.Client/DependencyInjection/StatisticsService.cs +++ b/Pal.Client/DependencyInjection/StatisticsService.cs @@ -33,7 +33,7 @@ namespace Pal.Client.DependencyInjection private async Task FetchFloorStatistics() { - if (!_configuration.HasRoleOnCurrentServer("statistics:view")) + if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view")) { _chatGui.PalError(Localization.Command_pal_stats_CurrentFloor); return; diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index fd2118e..67b1e71 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -296,7 +296,7 @@ namespace Pal.Client.Windows private void DrawExportTab() { - if (_configuration.HasRoleOnCurrentServer("export:run") && + if (_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "export:run") && ImGui.BeginTabItem($"{Localization.ConfigTab_Export}###TabExport")) { string todaysFileName = $"export-{DateTime.Today:yyyy-MM-dd}.pal"; From c7d5aa1eaadf1e7250d18431ca0baab8d6e26aeb Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 19:51:54 +0100 Subject: [PATCH 16/38] Db: Migrate ImportHistory to sqlite --- .../Configuration/ConfigurationManager.cs | 26 +++++- .../Configuration/IPalacePalConfiguration.cs | 3 - Pal.Client/Database/ImportHistory.cs | 12 +++ ...0230216154417_AddImportHistory.Designer.cs | 45 +++++++++ .../20230216154417_AddImportHistory.cs | 36 ++++++++ .../PalClientContextModelSnapshot.cs | 42 +++++++++ Pal.Client/Database/PalClientContext.cs | 14 +++ .../Database/PalClientContextFactory.cs | 20 ++++ .../DependencyInjection/ImportService.cs | 58 ++++++++++++ .../DependencyInjectionContext.cs | 91 ++++++++++++++++--- Pal.Client/LocalState.cs | 5 +- Pal.Client/Pal.Client.csproj | 41 +++++---- Pal.Client/Palace Pal.json | 2 +- Pal.Client/Plugin.cs | 3 - Pal.Client/README.md | 15 +++ Pal.Client/Scheduled/QueuedImport.cs | 40 ++++---- Pal.Client/Scheduled/QueuedUndoImport.cs | 12 ++- Pal.Client/Windows/ConfigWindow.cs | 21 ++++- 18 files changed, 415 insertions(+), 71 deletions(-) create mode 100644 Pal.Client/Database/ImportHistory.cs create mode 100644 Pal.Client/Database/Migrations/20230216154417_AddImportHistory.Designer.cs create mode 100644 Pal.Client/Database/Migrations/20230216154417_AddImportHistory.cs create mode 100644 Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs create mode 100644 Pal.Client/Database/PalClientContext.cs create mode 100644 Pal.Client/Database/PalClientContextFactory.cs create mode 100644 Pal.Client/DependencyInjection/ImportService.cs rename Pal.Client/{DependencyInjection => }/DependencyInjectionContext.cs (57%) create mode 100644 Pal.Client/README.md diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 2e8e284..2fae187 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -7,6 +7,8 @@ using System.Text.Json; using Dalamud.Logging; using Dalamud.Plugin; using ImGuiNET; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Database; using NJson = Newtonsoft.Json; namespace Pal.Client.Configuration @@ -14,12 +16,14 @@ namespace Pal.Client.Configuration internal sealed class ConfigurationManager { private readonly DalamudPluginInterface _pluginInterface; + private readonly IServiceProvider _serviceProvider; public event EventHandler? Saved; - public ConfigurationManager(DalamudPluginInterface pluginInterface) + public ConfigurationManager(DalamudPluginInterface pluginInterface, IServiceProvider serviceProvider) { _pluginInterface = pluginInterface; + _serviceProvider = serviceProvider; Migrate(); } @@ -61,6 +65,26 @@ namespace Pal.Client.Configuration var v7 = MigrateToV7(configurationV1); Save(v7, queue: false); + using (var scope = _serviceProvider.CreateScope()) + { + using var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Imports.RemoveRange(dbContext.Imports); + + foreach (var importHistory in configurationV1.ImportHistory) + { + PluginLog.Information($"Migrating import {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(); + } + File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); } } diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index 2752fce..c8a1ee8 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -23,9 +23,6 @@ namespace Pal.Client.Configuration DeepDungeonConfiguration DeepDungeons { get; set; } RendererConfiguration Renderer { get; set; } - [Obsolete] - List ImportHistory { get; } - IAccountConfiguration CreateAccount(string server, Guid accountId); IAccountConfiguration? FindAccount(string server); void RemoveAccount(string server); diff --git a/Pal.Client/Database/ImportHistory.cs b/Pal.Client/Database/ImportHistory.cs new file mode 100644 index 0000000..33e74f4 --- /dev/null +++ b/Pal.Client/Database/ImportHistory.cs @@ -0,0 +1,12 @@ +using System; + +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; } + } +} diff --git a/Pal.Client/Database/Migrations/20230216154417_AddImportHistory.Designer.cs b/Pal.Client/Database/Migrations/20230216154417_AddImportHistory.Designer.cs new file mode 100644 index 0000000..c65bf7e --- /dev/null +++ b/Pal.Client/Database/Migrations/20230216154417_AddImportHistory.Designer.cs @@ -0,0 +1,45 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Pal.Client.Database; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + [DbContext(typeof(PalClientContext))] + [Migration("20230216154417_AddImportHistory")] + partial class AddImportHistory + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ExportedAt") + .HasColumnType("TEXT"); + + b.Property("ImportedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Imports"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Pal.Client/Database/Migrations/20230216154417_AddImportHistory.cs b/Pal.Client/Database/Migrations/20230216154417_AddImportHistory.cs new file mode 100644 index 0000000..065048a --- /dev/null +++ b/Pal.Client/Database/Migrations/20230216154417_AddImportHistory.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + /// + public partial class AddImportHistory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Imports", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + RemoteUrl = table.Column(type: "TEXT", nullable: true), + ExportedAt = table.Column(type: "TEXT", nullable: false), + ImportedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Imports", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Imports"); + } + } +} diff --git a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs new file mode 100644 index 0000000..d6be610 --- /dev/null +++ b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs @@ -0,0 +1,42 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Pal.Client.Database; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + [DbContext(typeof(PalClientContext))] + partial class PalClientContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ExportedAt") + .HasColumnType("TEXT"); + + b.Property("ImportedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Imports"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Pal.Client/Database/PalClientContext.cs b/Pal.Client/Database/PalClientContext.cs new file mode 100644 index 0000000..b1c65e3 --- /dev/null +++ b/Pal.Client/Database/PalClientContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace Pal.Client.Database +{ + internal class PalClientContext : DbContext + { + public DbSet Imports { get; set; } = null!; + + public PalClientContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/Pal.Client/Database/PalClientContextFactory.cs b/Pal.Client/Database/PalClientContextFactory.cs new file mode 100644 index 0000000..30124b9 --- /dev/null +++ b/Pal.Client/Database/PalClientContextFactory.cs @@ -0,0 +1,20 @@ +#if EF +using System; +using System.IO; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Pal.Client.Database +{ + internal sealed class PalClientContextFactory : IDesignTimeDbContextFactory + { + public PalClientContext CreateDbContext(string[] args) + { + var optionsBuilder = + new DbContextOptionsBuilder().UseSqlite( + $"Data Source={Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "pluginConfigs", "Palace Pal", "palace-pal.data.sqlite3")}"); + return new PalClientContext(optionsBuilder.Options); + } + } +} +#endif diff --git a/Pal.Client/DependencyInjection/ImportService.cs b/Pal.Client/DependencyInjection/ImportService.cs new file mode 100644 index 0000000..a3c17ef --- /dev/null +++ b/Pal.Client/DependencyInjection/ImportService.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Database; + +namespace Pal.Client.DependencyInjection +{ + internal sealed class ImportService + { + private readonly IServiceProvider _serviceProvider; + + public ImportService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public void Add(ImportHistory history) + { + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + dbContext.Imports.Add(history); + dbContext.SaveChanges(); + } + + public ImportHistory? FindLast() + { + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + return dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefault(); + } + + public List FindForServer(string server) + { + if (string.IsNullOrEmpty(server)) + return new(); + + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList(); + } + + public void RemoveAllByIds(List ids) + { + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + dbContext.RemoveRange(dbContext.Imports.Where(x => ids.Contains(x.Id))); + dbContext.SaveChanges(); + } + + public void RemoveById(Guid id) + => RemoveAllByIds(new List { id }); + } +} diff --git a/Pal.Client/DependencyInjection/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs similarity index 57% rename from Pal.Client/DependencyInjection/DependencyInjectionContext.cs rename to Pal.Client/DependencyInjectionContext.cs index 6d2812f..7d4bbb1 100644 --- a/Pal.Client/DependencyInjection/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -1,4 +1,8 @@ -using System.Globalization; +using System; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; @@ -7,17 +11,22 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; +using Dalamud.Logging; using Dalamud.Plugin; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Pal.Client.Commands; using Pal.Client.Configuration; +using Pal.Client.Database; +using Pal.Client.DependencyInjection; using Pal.Client.Net; using Pal.Client.Properties; using Pal.Client.Rendering; using Pal.Client.Scheduled; using Pal.Client.Windows; -namespace Pal.Client.DependencyInjection +namespace Pal.Client { /// /// DI-aware Plugin. @@ -25,6 +34,8 @@ namespace Pal.Client.DependencyInjection // ReSharper disable once UnusedType.Global internal sealed class DependencyInjectionContext : IDalamudPlugin { + private readonly string _sqliteConnectionString; + private readonly CancellationTokenSource _initCts = new(); private ServiceProvider? _serviceProvider; public string Name => Localization.Palace_Pal; @@ -39,6 +50,9 @@ namespace Pal.Client.DependencyInjection CommandManager commandManager, DataManager dataManager) { + PluginLog.Information("Building service container"); + + CancellationToken token = _initCts.Token; IServiceCollection services = new ServiceCollection(); // dalamud @@ -54,6 +68,11 @@ namespace Pal.Client.DependencyInjection services.AddSingleton(dataManager); services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); + // EF core + _sqliteConnectionString = + $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; + services.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); + // plugin-specific services.AddSingleton(); services.AddSingleton(); @@ -64,11 +83,12 @@ namespace Pal.Client.DependencyInjection services.AddTransient(); services.AddSingleton(); - // territory handling + // territory & marker related services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // windows & related services services.AddSingleton(); @@ -97,7 +117,7 @@ namespace Pal.Client.DependencyInjection ValidateScopes = true, }); - // initialize plugin + #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 @@ -108,24 +128,61 @@ namespace Pal.Client.DependencyInjection _serviceProvider.GetService(); #endif - // set up legacy services - LocalState.PluginInterface = pluginInterface; - LocalState.Mode = _serviceProvider.GetRequiredService().Mode; + // 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. + PluginLog.Information("Service container built, triggering async init"); + Task.Run(async () => + { + try + { + PluginLog.Information("Starting async init"); - // windows that have logic to open on startup - _serviceProvider.GetRequiredService(); + // initialize database + await using (var scope = _serviceProvider.CreateAsyncScope()) + { + PluginLog.Log("Loading database & running migrations"); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + await dbContext.Database.MigrateAsync(); - // initialize components that are mostly self-contained/self-registered - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); + PluginLog.Log("Completed database migrations"); + } - _serviceProvider.GetRequiredService(); + token.ThrowIfCancellationRequested(); + + // set up legacy services + LocalState.PluginConfigDirectory = pluginInterface.GetPluginConfigDirectory(); + LocalState.Mode = _serviceProvider.GetRequiredService().Mode; + + // 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(); + _serviceProvider.GetRequiredService(); + + token.ThrowIfCancellationRequested(); + _serviceProvider.GetRequiredService(); + + PluginLog.Information("Async init complete"); + } + catch (Exception e) + { + PluginLog.Error(e, "Async load failed"); + chatGui.PrintError($"Async loading failed: {e}"); + } + }); } public void Dispose() { + _initCts.Cancel(); + // ensure we're not calling dispose recursively on ourselves if (_serviceProvider != null) { @@ -133,6 +190,10 @@ namespace Pal.Client.DependencyInjection _serviceProvider = null; serviceProvider.Dispose(); + + // 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/LocalState.cs b/Pal.Client/LocalState.cs index 41a125d..8534c50 100644 --- a/Pal.Client/LocalState.cs +++ b/Pal.Client/LocalState.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; -using Dalamud.Plugin; using Pal.Client.Configuration; using Pal.Client.Extensions; @@ -19,7 +18,7 @@ namespace Pal.Client private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; private const int CurrentVersion = 4; - internal static DalamudPluginInterface PluginInterface { get; set; } + internal static string PluginConfigDirectory { get; set; } = null!; internal static EMode Mode { get; set; } public uint TerritoryType { get; set; } @@ -126,7 +125,7 @@ namespace Pal.Client public string GetSaveLocation() => GetSaveLocation(TerritoryType); - private static string GetSaveLocation(uint territoryType) => Path.Join(PluginInterface.GetPluginConfigDirectory(), $"{territoryType}.json"); + private static string GetSaveLocation(uint territoryType) => Path.Join(PluginConfigDirectory, $"{territoryType}.json"); public static void ForEach(Action action) { diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index fd6869a..2edf178 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -25,15 +25,11 @@ - + - - - ResXFileCodeGenerator - Localization.Designer.cs - + @@ -68,44 +64,51 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll - false + false $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll - false + false $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll - false + false $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll - false + false $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll - false + false $(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll - false + false $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll - false + false - - True - True - Localization.resx - + + ResXFileCodeGenerator + Localization.Designer.cs + + + True + True + Localization.resx + - + + + + diff --git a/Pal.Client/Palace Pal.json b/Pal.Client/Palace Pal.json index 77ce336..79e7ff4 100644 --- a/Pal.Client/Palace Pal.json +++ b/Pal.Client/Palace Pal.json @@ -6,4 +6,4 @@ "RepoUrl": "https://github.com/carvelli/PalacePal", "IconUrl": "https://raw.githubusercontent.com/carvelli/Dalamud-Plugins/master/dist/Palace Pal.png", "Tags": [ "potd", "palace", "hoh", "splatoon" ] -} \ No newline at end of file +} diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index ddd2399..74cb8f4 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -5,7 +5,6 @@ using Pal.Client.Windows; using System; using System.Globalization; using System.Linq; -using Dalamud.Logging; using Pal.Client.Properties; using ECommons; using Microsoft.Extensions.DependencyInjection; @@ -31,8 +30,6 @@ namespace Pal.Client IPalacePalConfiguration configuration, RenderAdapter renderAdapter) { - PluginLog.Information("Initializing Palace Pal"); - _serviceProvider = serviceProvider; _pluginInterface = pluginInterface; _configuration = configuration; diff --git a/Pal.Client/README.md b/Pal.Client/README.md new file mode 100644 index 0000000..52bd208 --- /dev/null +++ b/Pal.Client/README.md @@ -0,0 +1,15 @@ +# Palace Pal + +## Client Build Notes + +### Database Migrations + +Since EF core needs all dll files to be present, including Dalamud ones, +there's a special `EF` configuration that exempts them from setting +`false` during the build. + +To use with `dotnet ef` commands, specify it as `-c EF`, for example: + +```shell +dotnet ef migrations add MigrationName --configuration EF +``` diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index ce13b30..afa99bf 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -7,10 +7,11 @@ using System.Linq; using System.Numerics; using Dalamud.Game.Gui; using Dalamud.Logging; -using Pal.Client.Configuration; +using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; using Pal.Client.Properties; +using Pal.Client.Windows; namespace Pal.Client.Scheduled { @@ -30,17 +31,20 @@ namespace Pal.Client.Scheduled internal sealed class Handler : IQueueOnFrameworkThread.Handler { private readonly ChatGui _chatGui; - private readonly IPalacePalConfiguration _configuration; - private readonly ConfigurationManager _configurationManager; private readonly FloorService _floorService; + private readonly ImportService _importService; + private readonly ConfigWindow _configWindow; - public Handler(ChatGui chatGui, IPalacePalConfiguration configuration, - ConfigurationManager configurationManager, FloorService floorService) + public Handler( + ChatGui chatGui, + FloorService floorService, + ImportService importService, + ConfigWindow configWindow) { _chatGui = chatGui; - _configuration = configuration; - _configurationManager = configurationManager; _floorService = floorService; + _importService = importService; + _configWindow = configWindow; } protected override void Run(QueuedImport import, ref bool recreateLayout, ref bool saveMarkers) @@ -53,11 +57,9 @@ namespace Pal.Client.Scheduled if (!Validate(import)) return; - var oldExportIds = string.IsNullOrEmpty(import.Export.ServerUrl) - ? _configuration.ImportHistory.Where(x => x.RemoteUrl == import.Export.ServerUrl) - .Select(x => x.Id) - .Where(x => x != Guid.Empty).ToList() - : new List(); + List oldExportIds = _importService.FindForServer(import.Export.ServerUrl) + .Select(x => x.Id) + .ToList(); foreach (var remoteFloor in import.Export.Floors) { @@ -70,17 +72,19 @@ namespace Pal.Client.Scheduled localState.Save(); } - _configuration.ImportHistory.RemoveAll(hist => - oldExportIds.Contains(hist.Id) || hist.Id == import.ExportId); - _configuration.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry + _importService.RemoveAllByIds(oldExportIds); + _importService.RemoveById(import.ExportId); + _importService.Add(new ImportHistory { Id = import.ExportId, RemoteUrl = import.Export.ServerUrl, ExportedAt = import.Export.CreatedAt.ToDateTime(), ImportedAt = DateTime.UtcNow, }); - _configurationManager.Save(_configuration); + _configWindow.UpdateLastImport(); + PluginLog.Information( + $"Imported {import.ExportId} for {import.ImportedTraps} traps, {import.ImportedHoardCoffers} hoard coffers"); _chatGui.PalMessage(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, import.ImportedHoardCoffers)); } @@ -101,9 +105,9 @@ namespace Pal.Client.Scheduled return false; } - if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || import.ExportId == Guid.Empty) + if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty) { - PluginLog.Error("Import: Invalid export id"); + PluginLog.Error($"Import: Invalid export id ({import.Export.ExportId})"); _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); return false; } diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index 37eb4c2..61b82b4 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Windows; using Pal.Common; namespace Pal.Client.Scheduled @@ -17,13 +18,15 @@ namespace Pal.Client.Scheduled internal sealed class Handler : IQueueOnFrameworkThread.Handler { - private readonly IPalacePalConfiguration _configuration; + private readonly ImportService _importService; private readonly FloorService _floorService; + private readonly ConfigWindow _configWindow; - public Handler(IPalacePalConfiguration configuration, FloorService floorService) + public Handler(ImportService importService, FloorService floorService, ConfigWindow configWindow) { - _configuration = configuration; + _importService = importService; _floorService = floorService; + _configWindow = configWindow; } protected override void Run(QueuedUndoImport queued, ref bool recreateLayout, ref bool saveMarkers) @@ -38,7 +41,8 @@ namespace Pal.Client.Scheduled localState.Save(); } - _configuration.ImportHistory.RemoveAll(hist => hist.Id == queued.ExportId); + _importService.RemoveById(queued.ExportId); + _configWindow.UpdateLastImport(); } } } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 67b1e71..bd0b00f 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -21,6 +21,7 @@ using System.Threading.Tasks; using Dalamud.Game.Gui; using Pal.Client.Properties; using Pal.Client.Configuration; +using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; @@ -40,6 +41,7 @@ namespace Pal.Client.Windows private readonly DebugState _debugState; private readonly ChatGui _chatGui; private readonly RemoteApi _remoteApi; + private readonly ImportService _importService; private int _mode; private int _renderer; @@ -55,6 +57,7 @@ namespace Pal.Client.Windows private string? _saveExportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); private readonly FileDialogManager _importDialog; private readonly FileDialogManager _exportDialog; + private ImportHistory? _lastImport; private CancellationTokenSource? _testConnectionCts; @@ -68,7 +71,8 @@ namespace Pal.Client.Windows FloorService floorService, DebugState debugState, ChatGui chatGui, - RemoteApi remoteApi) + RemoteApi remoteApi, + ImportService importService) : base(WindowId) { _windowSystem = windowSystem; @@ -81,6 +85,7 @@ namespace Pal.Client.Windows _debugState = debugState; _chatGui = chatGui; _remoteApi = remoteApi; + _importService = importService; LanguageChanged(); @@ -117,6 +122,8 @@ namespace Pal.Client.Windows _hoardConfig = new ConfigurableMarker(_configuration.DeepDungeons.HoardCoffers); _silverConfig = new ConfigurableMarker(_configuration.DeepDungeons.SilverCoffers); _connectionText = null; + + UpdateLastImport(); } public override void OnClose() @@ -278,13 +285,14 @@ namespace Pal.Client.Windows DoImport(_openImportPath); ImGui.EndDisabled(); - var importHistory = _configuration.ImportHistory.OrderByDescending(x => x.ImportedAt) - .ThenBy(x => x.Id).FirstOrDefault(); + ImportHistory? importHistory = _lastImport; if (importHistory != null) { ImGui.Separator(); ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, - importHistory.ImportedAt, importHistory.RemoteUrl, importHistory.ExportedAt)); + importHistory.ImportedAt.ToLocalTime(), + importHistory.RemoteUrl, + importHistory.ExportedAt.ToUniversalTime())); ImGui.TextWrapped(Localization.Config_UndoImportExplanation2); if (ImGui.Button(Localization.Config_UndoImport)) UndoImport(importHistory.Id); @@ -466,6 +474,11 @@ namespace Pal.Client.Windows _frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); } + internal void UpdateLastImport() + { + _lastImport = _importService.FindLast(); + } + private void DoExport(string destinationPath) { Task.Run(async () => From 29342264c04b0805ffba3c82f5f1db26334d6df4 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 20:40:32 +0100 Subject: [PATCH 17/38] Tweaks for pdb files --- Pal.Client/DalamudPackager.targets | 7 ++++--- Pal.Client/Pal.Client.csproj | 4 ++-- Pal.Common/Pal.Common.csproj | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Pal.Client/DalamudPackager.targets b/Pal.Client/DalamudPackager.targets index 2f4ce0f..27e6884 100644 --- a/Pal.Client/DalamudPackager.targets +++ b/Pal.Client/DalamudPackager.targets @@ -6,7 +6,7 @@ OutputPath="$(OutputPath)" AssemblyName="$(AssemblyName)" MakeZip="false" - VersionComponents="2"/> + VersionComponents="2"/> @@ -15,6 +15,7 @@ OutputPath="$(OutputPath)" AssemblyName="$(AssemblyName)" MakeZip="true" - VersionComponents="2"/> + VersionComponents="2" + Exclude="ECommons.pdb;ECommons.xml"/> - \ No newline at end of file + diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index 2edf178..4f876e5 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -16,12 +16,12 @@ false false true + portable + $(SolutionDir)=X:\ dist - none - false diff --git a/Pal.Common/Pal.Common.csproj b/Pal.Common/Pal.Common.csproj index 22d5a35..5166815 100644 --- a/Pal.Common/Pal.Common.csproj +++ b/Pal.Common/Pal.Common.csproj @@ -5,5 +5,7 @@ 11.0 enable enable + portable + $(SolutionDir)=X:\ From a5456a54a0001493b9bee712a476e81a4cac324b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 16 Feb 2023 22:09:29 +0100 Subject: [PATCH 18/38] DI: Cleanup --- Pal.Client/DependencyInjectionContext.cs | 5 ++++- Pal.Client/Plugin.cs | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 7d4bbb1..c7aa306 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -171,10 +171,13 @@ namespace Pal.Client PluginLog.Information("Async init complete"); } + catch (ObjectDisposedException) + { + } catch (Exception e) { PluginLog.Error(e, "Async load failed"); - chatGui.PrintError($"Async loading failed: {e}"); + chatGui.PrintError($"Async loading failed: {e.GetType()}: {e.Message}"); } }); } diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 74cb8f4..f30ff6b 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -23,17 +23,20 @@ namespace Pal.Client private readonly DalamudPluginInterface _pluginInterface; private readonly IPalacePalConfiguration _configuration; private readonly RenderAdapter _renderAdapter; + private readonly WindowSystem _windowSystem; public Plugin( IServiceProvider serviceProvider, DalamudPluginInterface pluginInterface, IPalacePalConfiguration configuration, - RenderAdapter renderAdapter) + RenderAdapter renderAdapter, + WindowSystem windowSystem) { _serviceProvider = serviceProvider; _pluginInterface = pluginInterface; _configuration = configuration; _renderAdapter = renderAdapter; + _windowSystem = windowSystem; LanguageChanged(pluginInterface.UiLanguage); @@ -63,14 +66,14 @@ namespace Pal.Client private void LanguageChanged(string languageCode) { Localization.Culture = new CultureInfo(languageCode); - _serviceProvider.GetRequiredService().Windows.OfType() + _windowSystem.Windows.OfType() .Each(w => w.LanguageChanged()); } private void Draw() { _renderAdapter.DrawLayers(); - _serviceProvider.GetRequiredService().Draw(); + _windowSystem.Draw(); } } } From 0bb7301ca144fea4bad455203d2c4522a002d2d2 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 00:54:23 +0100 Subject: [PATCH 19/38] Logging: Update PluginLog to use Microsoft.Extensions.Logging instead --- .../Configuration/AccountConfigurationV7.cs | 7 +++- Pal.Client/Configuration/ConfigurationData.cs | 7 +++- .../Configuration/ConfigurationManager.cs | 11 +++-- Pal.Client/Configuration/ConfigurationV1.cs | 7 ++-- .../DependencyInjection/ImportService.cs | 11 +++-- .../Logging/DalamudLogger.cs} | 40 ++++++++++++++----- .../Logging/DalamudLoggerProvider.cs | 31 ++++++++++++++ .../DependencyInjection/RepoVerification.cs | 7 ++-- Pal.Client/DependencyInjectionContext.cs | 29 ++++++++++---- Pal.Client/Hooks.cs | 7 +++- Pal.Client/Net/GrpcLoggerProvider.cs | 14 ------- Pal.Client/Net/RemoteApi.AccountService.cs | 35 ++++++++-------- Pal.Client/Net/RemoteApi.Utils.cs | 5 ++- Pal.Client/Net/RemoteApi.cs | 15 ++++--- Pal.Client/Rendering/SplatoonRenderer.cs | 14 +++++-- .../Scheduled/IQueueOnFrameworkThread.cs | 15 +++++-- Pal.Client/Scheduled/QueuedConfigUpdate.cs | 6 ++- Pal.Client/Scheduled/QueuedImport.cs | 15 ++++--- Pal.Client/Scheduled/QueuedSyncResponse.cs | 9 ++++- Pal.Client/Scheduled/QueuedUndoImport.cs | 4 +- Pal.Client/Windows/ConfigWindow.cs | 21 ++++++++-- 21 files changed, 216 insertions(+), 94 deletions(-) rename Pal.Client/{Net/GrpcLogger.cs => DependencyInjection/Logging/DalamudLogger.cs} (55%) create mode 100644 Pal.Client/DependencyInjection/Logging/DalamudLoggerProvider.cs delete mode 100644 Pal.Client/Net/GrpcLoggerProvider.cs diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index 4c65342..e02769a 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Security.Cryptography; using System.Text.Json.Serialization; using Dalamud.Logging; +using Microsoft.Extensions.Logging; +using Pal.Client.DependencyInjection.Logging; namespace Pal.Client.Configuration { @@ -10,6 +12,9 @@ namespace Pal.Client.Configuration { private const int DefaultEntropyLength = 16; + private static readonly ILogger _logger = + DependencyInjectionContext.LoggerProvider.CreateLogger(); + [JsonConstructor] public AccountConfigurationV7() { @@ -74,7 +79,7 @@ namespace Pal.Client.Configuration } catch (Exception e) { - PluginLog.Verbose(e, $"Could not load account id {EncryptedId}"); + _logger.LogTrace(e, "Could not load account id {Id}", EncryptedId); return null; } } diff --git a/Pal.Client/Configuration/ConfigurationData.cs b/Pal.Client/Configuration/ConfigurationData.cs index c93339c..cdb341c 100644 --- a/Pal.Client/Configuration/ConfigurationData.cs +++ b/Pal.Client/Configuration/ConfigurationData.cs @@ -2,11 +2,16 @@ using System; using System.Linq; using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using Pal.Client.DependencyInjection.Logging; namespace Pal.Client.Configuration { 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 }; @@ -30,7 +35,7 @@ namespace Pal.Client.Configuration _supportsDpapi = false; } - PluginLog.Verbose($"DPAPI support: {_supportsDpapi}"); + _logger.LogTrace("DPAPI support: {Supported}", _supportsDpapi); } return _supportsDpapi.Value; } diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 2fae187..4b4db7a 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -8,6 +8,7 @@ using Dalamud.Logging; using Dalamud.Plugin; using ImGuiNET; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Database; using NJson = Newtonsoft.Json; @@ -15,13 +16,15 @@ namespace Pal.Client.Configuration { internal sealed class ConfigurationManager { + private readonly ILogger _logger; private readonly DalamudPluginInterface _pluginInterface; private readonly IServiceProvider _serviceProvider; public event EventHandler? Saved; - public ConfigurationManager(DalamudPluginInterface pluginInterface, IServiceProvider serviceProvider) + public ConfigurationManager(ILogger logger, DalamudPluginInterface pluginInterface, IServiceProvider serviceProvider) { + _logger = logger; _pluginInterface = pluginInterface; _serviceProvider = serviceProvider; @@ -54,12 +57,12 @@ namespace Pal.Client.Configuration { if (_pluginInterface.ConfigFile.Exists) { - PluginLog.Information("Migrating config file from v1-v6 format"); + _logger.LogInformation("Migrating config file from v1-v6 format"); ConfigurationV1 configurationV1 = NJson.JsonConvert.DeserializeObject( File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1(); - configurationV1.Migrate(_pluginInterface); + configurationV1.Migrate(_pluginInterface, _serviceProvider.GetRequiredService>()); configurationV1.Save(_pluginInterface); var v7 = MigrateToV7(configurationV1); @@ -72,7 +75,7 @@ namespace Pal.Client.Configuration foreach (var importHistory in configurationV1.ImportHistory) { - PluginLog.Information($"Migrating import {importHistory.Id}"); + _logger.LogInformation("Migrating import {Id}", importHistory.Id); dbContext.Imports.Add(new ImportHistory { Id = importHistory.Id, diff --git a/Pal.Client/Configuration/ConfigurationV1.cs b/Pal.Client/Configuration/ConfigurationV1.cs index c824f33..ef98640 100644 --- a/Pal.Client/Configuration/ConfigurationV1.cs +++ b/Pal.Client/Configuration/ConfigurationV1.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Numerics; using Dalamud.Plugin; +using Microsoft.Extensions.Logging; namespace Pal.Client.Configuration { @@ -50,11 +51,11 @@ namespace Pal.Client.Configuration public string BetaKey { get; set; } = ""; #endregion - public void Migrate(DalamudPluginInterface pluginInterface) + public void Migrate(DalamudPluginInterface pluginInterface, ILogger logger) { if (Version == 1) { - PluginLog.Information("Updating config to version 2"); + logger.LogInformation("Updating config to version 2"); if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId)) AccountIds["http://localhost:5145"] = debugAccountId; @@ -68,7 +69,7 @@ namespace Pal.Client.Configuration if (Version == 2) { - PluginLog.Information("Updating config to version 3"); + logger.LogInformation("Updating config to version 3"); Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo { diff --git a/Pal.Client/DependencyInjection/ImportService.cs b/Pal.Client/DependencyInjection/ImportService.cs index a3c17ef..1d5f3cb 100644 --- a/Pal.Client/DependencyInjection/ImportService.cs +++ b/Pal.Client/DependencyInjection/ImportService.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Pal.Client.Database; @@ -24,12 +27,12 @@ namespace Pal.Client.DependencyInjection dbContext.SaveChanges(); } - public ImportHistory? FindLast() + public async Task FindLast(CancellationToken token = default) { - using var scope = _serviceProvider.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); + await using var scope = _serviceProvider.CreateAsyncScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); - return dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefault(); + return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefaultAsync(cancellationToken: token); } public List FindForServer(string server) diff --git a/Pal.Client/Net/GrpcLogger.cs b/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs similarity index 55% rename from Pal.Client/Net/GrpcLogger.cs rename to Pal.Client/DependencyInjection/Logging/DalamudLogger.cs index 717063c..329cfe7 100644 --- a/Pal.Client/Net/GrpcLogger.cs +++ b/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs @@ -1,27 +1,33 @@ using Dalamud.Logging; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Text; -namespace Pal.Client.Net +namespace Pal.Client.DependencyInjection.Logging { - internal sealed class GrpcLogger : ILogger + internal sealed class DalamudLogger : ILogger { private readonly string _name; + private readonly IExternalScopeProvider? _scopeProvider; - public GrpcLogger(string name) + public DalamudLogger(string name, IExternalScopeProvider? scopeProvider) { _name = name; + _scopeProvider = scopeProvider; } public IDisposable BeginScope(TState state) where TState : notnull - => NullScope.Instance; + => _scopeProvider?.Push(state) ?? NullScope.Instance; public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; - [MethodImpl(MethodImplOptions.NoInlining)] // PluginLog detects the plugin name as `Microsoft.Extensions.Logging` if inlined - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + // PluginLog detects the plugin name as `Microsoft.Extensions.Logging` if inlined + [MethodImpl(MethodImplOptions.NoInlining)] + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) { if (!IsEnabled(logLevel)) return; @@ -29,9 +35,23 @@ namespace Pal.Client.Net if (formatter == null) throw new ArgumentNullException(nameof(formatter)); - string message = $"gRPC[{_name}] {formatter(state, null)}"; - if (string.IsNullOrEmpty(message)) - return; + StringBuilder sb = new StringBuilder(); + _scopeProvider?.ForEachScope((scope, builder) => + { + if (scope is IEnumerable> properties) + { + foreach (KeyValuePair pair in properties) + { + builder.Append('<').Append(pair.Key).Append('=').Append(pair.Value) + .Append("> "); + } + } + else if (scope != null) + builder.Append('<').Append(scope).Append("> "); + }, + sb); + sb.Append('[').Append(_name).Append("] ").Append(formatter(state, null)); + string message = sb.ToString(); #pragma warning disable CS8604 // the nullability on PluginLog methods is wrong and allows nulls for exceptions, WriteLog even declares the parameter as `Exception? exception = null` switch (logLevel) @@ -63,7 +83,7 @@ namespace Pal.Client.Net #pragma warning restore CS8604 } - private class NullScope : IDisposable + private sealed class NullScope : IDisposable { public static NullScope Instance { get; } = new(); diff --git a/Pal.Client/DependencyInjection/Logging/DalamudLoggerProvider.cs b/Pal.Client/DependencyInjection/Logging/DalamudLoggerProvider.cs new file mode 100644 index 0000000..287239b --- /dev/null +++ b/Pal.Client/DependencyInjection/Logging/DalamudLoggerProvider.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace Pal.Client.DependencyInjection.Logging +{ + internal sealed class DalamudLoggerProvider : ILoggerProvider, ISupportExternalScope + { + private IExternalScopeProvider? _scopeProvider; + + public ILogger CreateLogger(string categoryName) => new DalamudLogger(categoryName, _scopeProvider); + + /// + /// Manual logger creation, doesn't handle scopes. + /// + public ILogger CreateLogger(Type type) => CreateLogger(type.FullName ?? type.ToString()); + + /// + /// Manual logger creation, doesn't handle scopes. + /// + public ILogger CreateLogger() => CreateLogger(typeof(T)); + + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; + } + + public void Dispose() + { + } + } +} diff --git a/Pal.Client/DependencyInjection/RepoVerification.cs b/Pal.Client/DependencyInjection/RepoVerification.cs index db46b64..63a3d16 100644 --- a/Pal.Client/DependencyInjection/RepoVerification.cs +++ b/Pal.Client/DependencyInjection/RepoVerification.cs @@ -2,16 +2,17 @@ using Dalamud.Game.Gui; using Dalamud.Logging; using Dalamud.Plugin; +using Microsoft.Extensions.Logging; using Pal.Client.Extensions; using Pal.Client.Properties; namespace Pal.Client.DependencyInjection { - public class RepoVerification + internal sealed class RepoVerification { - public RepoVerification(DalamudPluginInterface pluginInterface, ChatGui chatGui) + public RepoVerification(ILogger logger, DalamudPluginInterface pluginInterface, ChatGui chatGui) { - PluginLog.Information($"Install source: {pluginInterface.SourceRepository}"); + 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/")) diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index c7aa306..bb5b23a 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -16,10 +16,12 @@ using Dalamud.Plugin; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Commands; using Pal.Client.Configuration; using Pal.Client.Database; using Pal.Client.DependencyInjection; +using Pal.Client.DependencyInjection.Logging; using Pal.Client.Net; using Pal.Client.Properties; using Pal.Client.Rendering; @@ -34,6 +36,9 @@ namespace Pal.Client // ReSharper disable once UnusedType.Global internal sealed class DependencyInjectionContext : IDalamudPlugin { + public static DalamudLoggerProvider LoggerProvider { get; } = new(); + + private readonly ILogger _logger = LoggerProvider.CreateLogger(); private readonly string _sqliteConnectionString; private readonly CancellationTokenSource _initCts = new(); private ServiceProvider? _serviceProvider; @@ -50,11 +55,16 @@ namespace Pal.Client CommandManager commandManager, DataManager dataManager) { - PluginLog.Information("Building service container"); + // Temporary logger, will be overriden later + _logger.LogInformation("Building service container"); CancellationToken token = _initCts.Token; IServiceCollection services = new ServiceCollection(); - + services.AddLogging(builder => + builder.AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) + .AddFilter("Grpc", LogLevel.Debug) + .ClearProviders() + .AddProvider(LoggerProvider)); // dalamud services.AddSingleton(this); services.AddSingleton(pluginInterface); @@ -134,21 +144,24 @@ namespace Pal.Client // // There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for // config stuff. - PluginLog.Information("Service container built, triggering async init"); + _logger = _serviceProvider.GetRequiredService>(); + _logger.LogInformation("Service container built, triggering async init"); Task.Run(async () => { + using IDisposable? logScope = _logger.BeginScope("AsyncInit"); + try { - PluginLog.Information("Starting async init"); + _logger.LogInformation("Starting async init"); // initialize database await using (var scope = _serviceProvider.CreateAsyncScope()) { - PluginLog.Log("Loading database & running migrations"); + _logger.LogInformation("Loading database & running migrations"); await using var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync(); - PluginLog.Log("Completed database migrations"); + _logger.LogInformation("Completed database migrations"); } token.ThrowIfCancellationRequested(); @@ -169,14 +182,14 @@ namespace Pal.Client token.ThrowIfCancellationRequested(); _serviceProvider.GetRequiredService(); - PluginLog.Information("Async init complete"); + _logger.LogInformation("Async init complete"); } catch (ObjectDisposedException) { } catch (Exception e) { - PluginLog.Error(e, "Async load failed"); + _logger.LogError(e, "Async load failed"); chatGui.PrintError($"Async loading failed: {e.GetType()}: {e.Message}"); } }); diff --git a/Pal.Client/Hooks.cs b/Pal.Client/Hooks.cs index 24aa439..ae197fb 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -6,12 +6,14 @@ using Dalamud.Utility.Signatures; using System; using System.Text; using Dalamud.Game.ClientState.Objects; +using Microsoft.Extensions.Logging; using Pal.Client.DependencyInjection; namespace Pal.Client { internal sealed unsafe class Hooks : IDisposable { + private readonly ILogger _logger; private readonly ObjectTable _objectTable; private readonly TerritoryState _territoryState; private readonly FrameworkService _frameworkService; @@ -23,8 +25,9 @@ namespace Pal.Client private Hook ActorVfxCreateHook { get; init; } = null!; #pragma warning restore CS0649 - public Hooks(ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) + public Hooks(ILogger logger, ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) { + _logger = logger; _objectTable = objectTable; _territoryState = territoryState; _frameworkService = frameworkService; @@ -86,7 +89,7 @@ namespace Pal.Client } catch (Exception e) { - PluginLog.Error(e, "VFX Create Hook failed"); + _logger.LogError(e, "VFX Create Hook failed"); } return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7); } diff --git a/Pal.Client/Net/GrpcLoggerProvider.cs b/Pal.Client/Net/GrpcLoggerProvider.cs deleted file mode 100644 index f9db63f..0000000 --- a/Pal.Client/Net/GrpcLoggerProvider.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; - -namespace Pal.Client.Net -{ - internal sealed class GrpcLoggerProvider : ILoggerProvider - { - public ILogger CreateLogger(string categoryName) => new GrpcLogger(categoryName); - - public void Dispose() - { - } - } -} diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index 747709d..bc244cf 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -1,5 +1,4 @@ using Account; -using Dalamud.Logging; using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.Logging; @@ -19,9 +18,11 @@ namespace Pal.Client.Net { private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) { + using IDisposable? logScope = _logger.BeginScope("TryConnect"); + if (_configuration.Mode != EMode.Online) { - PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection"); + _logger.LogDebug("Not Online, not attempting to establish a connection"); return (false, Localization.ConnectionError_NotOnline); } @@ -29,7 +30,7 @@ namespace Pal.Client.Net { Dispose(); - PluginLog.Information("TryConnect: Creating new gRPC channel"); + _logger.LogInformation("Creating new gRPC channel"); _channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions { HttpHandler = new SocketsHttpHandler @@ -40,7 +41,7 @@ namespace Pal.Client.Net LoggerFactory = loggerFactory, }); - PluginLog.Information($"TryConnect: Connecting to upstream service at {RemoteUrl}"); + _logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl); await _channel.ConnectAsync(cancellationToken); } @@ -50,7 +51,7 @@ namespace Pal.Client.Net IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl); if (configuredAccount == null) { - PluginLog.Information($"TryConnect: No account information saved for {RemoteUrl}, creating new account"); + _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) { @@ -58,13 +59,13 @@ namespace Pal.Client.Net throw new InvalidOperationException("invalid account id returned"); configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); - PluginLog.Information($"TryConnect: Account created with id {accountId.ToPartialId()}"); + _logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId()); _configurationManager.Save(_configuration); } else { - PluginLog.Error($"TryConnect: Account creation failed with error {createAccountReply.Error}"); + _logger.LogError("Account creation failed with error {Error}", createAccountReply.Error); if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) { _chatGui.PalError(Localization.ConnectionError_OldVersion); @@ -79,17 +80,17 @@ namespace Pal.Client.Net // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (configuredAccount == null) { - PluginLog.Warning("TryConnect: No account to login with"); + _logger.LogWarning("No account to login with"); return (false, Localization.ConnectionError_CreateAccountReturnedNoId); } if (!_loginInfo.IsValid) { - PluginLog.Information($"TryConnect: Logging in with account id {configuredAccount.AccountId.ToPartialId()}"); + _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) { - PluginLog.Information($"TryConnect: Login successful with account id: {configuredAccount.AccountId.ToPartialId()}"); + _logger.LogInformation("Login successful with account id: {AccountId}", configuredAccount.AccountId.ToPartialId()); _loginInfo = new LoginInfo(loginReply.AuthToken); bool save = configuredAccount.EncryptIfNeeded(); @@ -106,7 +107,7 @@ namespace Pal.Client.Net } else { - PluginLog.Error($"TryConnect: Login failed with error {loginReply.Error}"); + _logger.LogError("Login failed with error {Error}", loginReply.Error); _loginInfo = new LoginInfo(null); if (loginReply.Error == LoginError.InvalidAccountId) { @@ -114,7 +115,7 @@ namespace Pal.Client.Net _configurationManager.Save(_configuration); if (retry) { - PluginLog.Information("TryConnect: Attempting connection retry without account id"); + _logger.LogInformation("Attempting connection retry without account id"); return await TryConnect(cancellationToken, retry: false); } else @@ -131,7 +132,7 @@ namespace Pal.Client.Net if (!_loginInfo.IsValid) { - PluginLog.Error($"TryConnect: Login state is loggedIn={_loginInfo.IsLoggedIn}, expired={_loginInfo.IsExpired}"); + _logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, _loginInfo.IsExpired); return (false, Localization.ConnectionError_LoginReturnedNoToken); } @@ -147,17 +148,19 @@ namespace Pal.Client.Net public async Task VerifyConnection(CancellationToken cancellationToken = default) { + using IDisposable? logScope = _logger.BeginScope("VerifyConnection"); + _warnedAboutUpgrade = false; - var connectionResult = await TryConnect(cancellationToken, loggerFactory: _grpcToPluginLogLoggerFactory); + var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory); if (!connectionResult.Success) return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error); - PluginLog.Information("VerifyConnection: Connection established, trying to verify auth token"); + _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); - PluginLog.Information("VerifyConnection: Verification returned no errors."); + _logger.LogInformation("Verification returned no errors."); return Localization.ConnectionSuccessful; } diff --git a/Pal.Client/Net/RemoteApi.Utils.cs b/Pal.Client/Net/RemoteApi.Utils.cs index 045adb8..83a1180 100644 --- a/Pal.Client/Net/RemoteApi.Utils.cs +++ b/Pal.Client/Net/RemoteApi.Utils.cs @@ -3,6 +3,7 @@ using Dalamud.Logging; using Grpc.Core; using System.Net.Security; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; namespace Pal.Client.Net { @@ -40,7 +41,7 @@ namespace Pal.Client.Net throw new InvalidOperationException(); var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet); - PluginLog.Debug($"Using client certificate {certificate.GetCertHashString()}"); + _logger.LogDebug("Using client certificate {CertificateHash}", certificate.GetCertHashString()); return new SslClientAuthenticationOptions { ClientCertificates = new X509CertificateCollection() @@ -49,7 +50,7 @@ namespace Pal.Client.Net }, }; #else - PluginLog.Debug("Not using client certificate"); + _logger.LogDebug("Not using client certificate"); return null; #endif } diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 9315941..e858308 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -17,9 +17,8 @@ namespace Pal.Client.Net private readonly string _userAgent = $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; - private readonly ILoggerFactory _grpcToPluginLogLoggerFactory = LoggerFactory.Create(builder => - builder.AddProvider(new GrpcLoggerProvider()).AddFilter("Grpc", LogLevel.Trace)); - + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; private readonly ChatGui _chatGui; private readonly ConfigurationManager _configurationManager; private readonly IPalacePalConfiguration _configuration; @@ -28,9 +27,15 @@ namespace Pal.Client.Net private LoginInfo _loginInfo = new(null); private bool _warnedAboutUpgrade; - public RemoteApi(ChatGui chatGui, ConfigurationManager configurationManager, + public RemoteApi( + ILoggerFactory loggerFactory, + ILogger logger, + ChatGui chatGui, + ConfigurationManager configurationManager, IPalacePalConfiguration configuration) { + _loggerFactory = loggerFactory; + _logger = logger; _chatGui = chatGui; _configurationManager = configurationManager; _configuration = configuration; @@ -38,7 +43,7 @@ namespace Pal.Client.Net public void Dispose() { - PluginLog.Debug("Disposing gRPC channel"); + _logger.LogDebug("Disposing gRPC channel"); _channel?.Dispose(); _channel = null; } diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 4ebd659..5286b81 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -13,6 +13,7 @@ using System.Numerics; using System.Reflection; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; +using Microsoft.Extensions.Logging; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; @@ -22,19 +23,24 @@ namespace Pal.Client.Rendering { private const long OnTerritoryChange = -2; + private readonly ILogger _logger; private readonly DebugState _debugState; private readonly ClientState _clientState; private readonly ChatGui _chatGui; - public SplatoonRenderer(DalamudPluginInterface pluginInterface, IDalamudPlugin dalamudPlugin, + public SplatoonRenderer( + ILogger logger, + DalamudPluginInterface pluginInterface, + IDalamudPlugin dalamudPlugin, DebugState debugState, ClientState clientState, ChatGui chatGui) { + _logger = logger; _debugState = debugState; _clientState = clientState; _chatGui = chatGui; - PluginLog.Information("Initializing splatoon..."); + _logger.LogInformation("Initializing splatoon..."); ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI); } @@ -53,7 +59,7 @@ namespace Pal.Client.Rendering } catch (Exception e) { - PluginLog.Error(e, $"Could not create splatoon layer {layer} with {elements.Count} elements"); + _logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer, elements.Count); _debugState.SetFromException(e); } }); @@ -67,7 +73,7 @@ namespace Pal.Client.Rendering } catch (Exception e) { - PluginLog.Error(e, $"Could not reset splatoon layer {layer}"); + _logger.LogError(e, "Could not reset splatoon layer {Layer}", layer); } } diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index a8d4aeb..2796971 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -1,4 +1,6 @@ -using Dalamud.Logging; +using System.Reflection.Metadata; +using Dalamud.Logging; +using Microsoft.Extensions.Logging; namespace Pal.Client.Scheduled { @@ -12,18 +14,25 @@ namespace Pal.Client.Scheduled internal abstract class Handler : IHandler where T : IQueueOnFrameworkThread { + protected readonly ILogger> _logger; + + protected Handler(ILogger> logger) + { + _logger = logger; + } + protected abstract void Run(T queued, ref bool recreateLayout, ref bool saveMarkers); public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) { if (queued is T t) { - PluginLog.Information($"Handling {queued.GetType()} with handler {GetType()}"); + _logger.LogInformation("Handling {QueuedType}", queued.GetType()); Run(t, ref recreateLayout, ref saveMarkers); } else { - PluginLog.Error($"Could not use queue handler {GetType()} with type {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 67c04a1..3a3e802 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -1,4 +1,5 @@ -using Pal.Client.Configuration; +using Microsoft.Extensions.Logging; +using Pal.Client.Configuration; using Pal.Client.DependencyInjection; namespace Pal.Client.Scheduled @@ -11,8 +12,9 @@ namespace Pal.Client.Scheduled private readonly FloorService _floorService; private readonly TerritoryState _territoryState; - public Handler(IPalacePalConfiguration configuration, FloorService floorService, + public Handler(ILogger logger, IPalacePalConfiguration configuration, FloorService floorService, TerritoryState territoryState) + : base(logger) { _configuration = configuration; _floorService = floorService; diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index afa99bf..fa4cb93 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Numerics; using Dalamud.Game.Gui; using Dalamud.Logging; +using Microsoft.Extensions.Logging; using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; @@ -36,10 +37,12 @@ namespace Pal.Client.Scheduled private readonly ConfigWindow _configWindow; public Handler( + ILogger logger, ChatGui chatGui, FloorService floorService, ImportService importService, ConfigWindow configWindow) + : base(logger) { _chatGui = chatGui; _floorService = floorService; @@ -83,14 +86,14 @@ namespace Pal.Client.Scheduled }); _configWindow.UpdateLastImport(); - PluginLog.Information( + _logger.LogInformation( $"Imported {import.ExportId} for {import.ImportedTraps} traps, {import.ImportedHoardCoffers} hoard coffers"); _chatGui.PalMessage(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, import.ImportedHoardCoffers)); } catch (Exception e) { - PluginLog.Error(e, "Import failed"); + _logger.LogError(e, "Import failed"); _chatGui.PalError(string.Format(Localization.Error_ImportFailed, e)); } } @@ -99,15 +102,15 @@ namespace Pal.Client.Scheduled { if (import.Export.ExportVersion != ExportConfig.ExportVersion) { - PluginLog.Error( - $"Import: Different version in export file, {import.Export.ExportVersion} != {ExportConfig.ExportVersion}"); + _logger.LogError( + "Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}", import.Export.ExportVersion, ExportConfig.ExportVersion); _chatGui.PalError(Localization.Error_ImportFailed_IncompatibleVersion); return false; } if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty) { - PluginLog.Error($"Import: Invalid export id ({import.Export.ExportId})"); + _logger.LogError("Import: Invalid export id '{Id}'", import.Export.ExportId); _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); return false; } @@ -117,7 +120,7 @@ namespace Pal.Client.Scheduled if (string.IsNullOrEmpty(import.Export.ServerUrl)) { // If we allow for backups as import/export, this should be removed - PluginLog.Error("Import: No server URL"); + _logger.LogError("Import: No server URL"); _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); return false; } diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index 442cb87..edf72ef 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; @@ -22,7 +23,13 @@ namespace Pal.Client.Scheduled private readonly TerritoryState _territoryState; private readonly DebugState _debugState; - public Handler(IPalacePalConfiguration configuration, FloorService floorService, TerritoryState territoryState, DebugState debugState) + public Handler( + ILogger logger, + IPalacePalConfiguration configuration, + FloorService floorService, + TerritoryState territoryState, + DebugState debugState) + : base(logger) { _configuration = configuration; _floorService = floorService; diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index 61b82b4..9e0eb2c 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; using Pal.Client.Windows; @@ -22,7 +23,8 @@ namespace Pal.Client.Scheduled private readonly FloorService _floorService; private readonly ConfigWindow _configWindow; - public Handler(ImportService importService, FloorService floorService, ConfigWindow configWindow) + public Handler(ILogger logger, ImportService importService, FloorService floorService, ConfigWindow configWindow) + : base(logger) { _importService = importService; _floorService = floorService; diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index bd0b00f..84a8889 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -19,6 +19,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Dalamud.Game.Gui; +using Microsoft.Extensions.Logging; using Pal.Client.Properties; using Pal.Client.Configuration; using Pal.Client.Database; @@ -31,6 +32,7 @@ namespace Pal.Client.Windows { private const string WindowId = "###PalPalaceConfig"; + private readonly ILogger _logger; private readonly WindowSystem _windowSystem; private readonly ConfigurationManager _configurationManager; private readonly IPalacePalConfiguration _configuration; @@ -60,8 +62,10 @@ namespace Pal.Client.Windows private ImportHistory? _lastImport; private CancellationTokenSource? _testConnectionCts; + private CancellationTokenSource? _lastImportCts; public ConfigWindow( + ILogger logger, WindowSystem windowSystem, ConfigurationManager configurationManager, IPalacePalConfiguration configuration, @@ -75,6 +79,7 @@ namespace Pal.Client.Windows ImportService importService) : base(WindowId) { + _logger = logger; _windowSystem = windowSystem; _configurationManager = configurationManager; _configuration = configuration; @@ -105,6 +110,7 @@ namespace Pal.Client.Windows public void Dispose() { _windowSystem.RemoveWindow(this); + _lastImportCts?.Cancel(); _testConnectionCts?.Cancel(); } @@ -454,11 +460,11 @@ namespace Pal.Client.Windows { if (cts == _testConnectionCts) { - PluginLog.Error(e, "Could not establish remote connection"); + _logger.LogError(e, "Could not establish remote connection"); _connectionText = e.ToString(); } else - PluginLog.Warning(e, + _logger.LogWarning(e, "Could not establish a remote connection, but user also clicked 'test connection' again so not updating UI"); } }); @@ -476,7 +482,14 @@ namespace Pal.Client.Windows internal void UpdateLastImport() { - _lastImport = _importService.FindLast(); + _lastImportCts?.Cancel(); + CancellationTokenSource cts = new CancellationTokenSource(); + _lastImportCts = cts; + + Task.Run(async () => + { + _lastImport = await _importService.FindLast(cts.Token); + }, cts.Token); } private void DoExport(string destinationPath) @@ -500,7 +513,7 @@ namespace Pal.Client.Windows } catch (Exception e) { - PluginLog.Error(e, "Export failed"); + _logger.LogError(e, "Export failed"); _chatGui.PalError($"Export failed: {e}"); } }); From 8b6dd52b5438ef5400133530f9e298de8d704502 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 13:57:12 +0100 Subject: [PATCH 20/38] Logging: Use underlying serilog logger directly --- .../Logging/DalamudLogger.cs | 54 +++++++------------ Pal.Client/DependencyInjectionContext.cs | 3 +- Pal.Client/Pal.Client.csproj | 4 ++ 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs b/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs index 329cfe7..1d65fc6 100644 --- a/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs +++ b/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs @@ -1,14 +1,16 @@ -using Dalamud.Logging; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; +using Serilog.Events; namespace Pal.Client.DependencyInjection.Logging { internal sealed class DalamudLogger : ILogger { + private static readonly string AssemblyName = typeof(Plugin).Assembly.GetName().Name!; + private static readonly Serilog.ILogger PluginLogDelegate = Serilog.Log.ForContext("SourceContext", AssemblyName); private readonly string _name; private readonly IExternalScopeProvider? _scopeProvider; @@ -22,10 +24,8 @@ namespace Pal.Client.DependencyInjection.Logging where TState : notnull => _scopeProvider?.Push(state) ?? NullScope.Instance; - public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && PluginLogDelegate.IsEnabled(ToSerilogLevel(logLevel)); - // PluginLog detects the plugin name as `Microsoft.Extensions.Logging` if inlined - [MethodImpl(MethodImplOptions.NoInlining)] public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { @@ -36,6 +36,7 @@ namespace Pal.Client.DependencyInjection.Logging throw new ArgumentNullException(nameof(formatter)); StringBuilder sb = new StringBuilder(); + sb.Append('[').Append(AssemblyName).Append("] "); _scopeProvider?.ForEachScope((scope, builder) => { if (scope is IEnumerable> properties) @@ -50,37 +51,22 @@ namespace Pal.Client.DependencyInjection.Logging builder.Append('<').Append(scope).Append("> "); }, sb); - sb.Append('[').Append(_name).Append("] ").Append(formatter(state, null)); - string message = sb.ToString(); + sb.Append(_name).Append(": ").Append(formatter(state, null)); + PluginLogDelegate.Write(ToSerilogLevel(logLevel), exception, sb.ToString()); + } -#pragma warning disable CS8604 // the nullability on PluginLog methods is wrong and allows nulls for exceptions, WriteLog even declares the parameter as `Exception? exception = null` - switch (logLevel) + private LogEventLevel ToSerilogLevel(LogLevel logLevel) + { + return logLevel switch { - case LogLevel.Critical: - PluginLog.Fatal(exception, message); - break; - - case LogLevel.Error: - PluginLog.Error(exception, message); - break; - - case LogLevel.Warning: - PluginLog.Warning(exception, message); - break; - - case LogLevel.Information: - PluginLog.Information(exception, message); - break; - - case LogLevel.Debug: - PluginLog.Debug(exception, message); - break; - - case LogLevel.Trace: - PluginLog.Verbose(exception, message); - break; - } -#pragma warning restore CS8604 + LogLevel.Critical => LogEventLevel.Fatal, + LogLevel.Error => LogEventLevel.Error, + LogLevel.Warning => LogEventLevel.Warning, + LogLevel.Information => LogEventLevel.Information, + LogLevel.Debug => LogEventLevel.Debug, + LogLevel.Trace => LogEventLevel.Verbose, + _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null) + }; } private sealed class NullScope : IDisposable diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index bb5b23a..bc8df78 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -61,7 +61,8 @@ namespace Pal.Client CancellationToken token = _initCts.Token; IServiceCollection services = new ServiceCollection(); services.AddLogging(builder => - builder.AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) + builder.AddFilter("Pal", LogLevel.Trace) + .AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) .AddFilter("Grpc", LogLevel.Debug) .ClearProviders() .AddProvider(LoggerProvider)); diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index 0387c60..bb1d9a9 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -90,6 +90,10 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll false + + $(AppData)\XIVLauncher\addon\Hooks\dev\Serilog.dll + false + From 870f29f0c67af01a39400046586c55ad3dcff602 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 15:51:45 +0100 Subject: [PATCH 21/38] DI: Add Chat instead of using ChatExtensions --- Pal.Client/Commands/PalCommand.cs | 18 +++++++-------- .../Chat.cs} | 22 ++++++++++++++----- Pal.Client/DependencyInjection/ChatService.cs | 2 +- .../DependencyInjection/RepoVerification.cs | 4 ++-- .../DependencyInjection/StatisticsService.cs | 14 ++++++------ Pal.Client/DependencyInjectionContext.cs | 6 ++++- Pal.Client/Hooks.cs | 2 +- Pal.Client/Net/RemoteApi.AccountService.cs | 7 +++--- Pal.Client/Net/RemoteApi.cs | 7 +++--- Pal.Client/Rendering/SplatoonRenderer.cs | 18 +++++++++------ Pal.Client/Scheduled/QueuedImport.cs | 16 +++++++------- Pal.Client/Windows/ConfigWindow.cs | 12 +++++----- 12 files changed, 74 insertions(+), 54 deletions(-) rename Pal.Client/{Extensions/ChatExtensions.cs => DependencyInjection/Chat.cs} (52%) diff --git a/Pal.Client/Commands/PalCommand.cs b/Pal.Client/Commands/PalCommand.cs index 336623f..bd20305 100644 --- a/Pal.Client/Commands/PalCommand.cs +++ b/Pal.Client/Commands/PalCommand.cs @@ -19,7 +19,7 @@ namespace Pal.Client.Commands { private readonly IPalacePalConfiguration _configuration; private readonly CommandManager _commandManager; - private readonly ChatGui _chatGui; + private readonly Chat _chat; private readonly StatisticsService _statisticsService; private readonly ConfigWindow _configWindow; private readonly TerritoryState _territoryState; @@ -29,7 +29,7 @@ namespace Pal.Client.Commands public PalCommand( IPalacePalConfiguration configuration, CommandManager commandManager, - ChatGui chatGui, + Chat chat, StatisticsService statisticsService, ConfigWindow configWindow, TerritoryState territoryState, @@ -38,7 +38,7 @@ namespace Pal.Client.Commands { _configuration = configuration; _commandManager = commandManager; - _chatGui = chatGui; + _chat = chat; _statisticsService = statisticsService; _configWindow = configWindow; _territoryState = territoryState; @@ -60,7 +60,7 @@ namespace Pal.Client.Commands { if (_configuration.FirstUse) { - _chatGui.PalError(Localization.Error_FirstTimeSetupRequired); + _chat.Error(Localization.Error_FirstTimeSetupRequired); return; } @@ -82,7 +82,7 @@ namespace Pal.Client.Commands #if DEBUG case "update-saves": LocalState.UpdateAll(); - _chatGui.PalMessage(Localization.Command_pal_updatesaves); + _chat.Message(Localization.Command_pal_updatesaves); break; #endif @@ -104,14 +104,14 @@ namespace Pal.Client.Commands break; default: - _chatGui.PalError(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, + _chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, command)); break; } } catch (Exception e) { - _chatGui.PalError(e.ToString()); + _chat.Error(e.ToString()); } } @@ -124,7 +124,7 @@ namespace Pal.Client.Commands var playerPosition = _clientState.LocalPlayer?.Position; if (playerPosition == null) return; - _chatGui.PalMessage($"{playerPosition}"); + _chat.Message($"{playerPosition}"); var nearbyMarkers = state.Markers .Where(m => predicate(m)) @@ -134,7 +134,7 @@ namespace Pal.Client.Commands .Take(5) .ToList(); foreach (var nearbyMarker in nearbyMarkers) - _chatGui.Print( + _chat.UnformattedMessage( $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); } } diff --git a/Pal.Client/Extensions/ChatExtensions.cs b/Pal.Client/DependencyInjection/Chat.cs similarity index 52% rename from Pal.Client/Extensions/ChatExtensions.cs rename to Pal.Client/DependencyInjection/Chat.cs index ff7cac4..05a91e1 100644 --- a/Pal.Client/Extensions/ChatExtensions.cs +++ b/Pal.Client/DependencyInjection/Chat.cs @@ -3,13 +3,20 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Pal.Client.Properties; -namespace Pal.Client.Extensions +namespace Pal.Client.DependencyInjection { - public static class ChatExtensions + internal sealed class Chat { - public static void PalError(this ChatGui chat, string e) + private readonly ChatGui _chatGui; + + public Chat(ChatGui chatGui) { - chat.PrintChat(new XivChatEntry + _chatGui = chatGui; + } + + public void Error(string e) + { + _chatGui.PrintChat(new XivChatEntry { Message = new SeStringBuilder() .AddUiForeground($"[{Localization.Palace_Pal}] ", 16) @@ -18,11 +25,14 @@ namespace Pal.Client.Extensions }); } - public static void PalMessage(this ChatGui chat, string message) + public void Message(string message) { - chat.Print(new SeStringBuilder() + _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 2dcfeb2..4b40b82 100644 --- a/Pal.Client/DependencyInjection/ChatService.cs +++ b/Pal.Client/DependencyInjection/ChatService.cs @@ -91,7 +91,7 @@ namespace Pal.Client.DependencyInjection return _dataManager.GetExcelSheet()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; } - private class LocalizedChatMessages + 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!"; diff --git a/Pal.Client/DependencyInjection/RepoVerification.cs b/Pal.Client/DependencyInjection/RepoVerification.cs index 63a3d16..07c41bc 100644 --- a/Pal.Client/DependencyInjection/RepoVerification.cs +++ b/Pal.Client/DependencyInjection/RepoVerification.cs @@ -10,14 +10,14 @@ namespace Pal.Client.DependencyInjection { internal sealed class RepoVerification { - public RepoVerification(ILogger logger, DalamudPluginInterface pluginInterface, ChatGui chatGui) + 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/")) { - chatGui.PalError(string.Format(Localization.Error_WrongRepository, + chat.Error(string.Format(Localization.Error_WrongRepository, "https://github.com/carvelli/Dalamud-Plugins")); throw new InvalidOperationException(); } diff --git a/Pal.Client/DependencyInjection/StatisticsService.cs b/Pal.Client/DependencyInjection/StatisticsService.cs index b7ee3cf..2096ba8 100644 --- a/Pal.Client/DependencyInjection/StatisticsService.cs +++ b/Pal.Client/DependencyInjection/StatisticsService.cs @@ -15,15 +15,15 @@ namespace Pal.Client.DependencyInjection private readonly IPalacePalConfiguration _configuration; private readonly RemoteApi _remoteApi; private readonly StatisticsWindow _statisticsWindow; - private readonly ChatGui _chatGui; + private readonly Chat _chat; public StatisticsService(IPalacePalConfiguration configuration, RemoteApi remoteApi, - StatisticsWindow statisticsWindow, ChatGui chatGui) + StatisticsWindow statisticsWindow, Chat chat) { _configuration = configuration; _remoteApi = remoteApi; _statisticsWindow = statisticsWindow; - _chatGui = chatGui; + _chat = chat; } public void ShowGlobalStatistics() @@ -35,7 +35,7 @@ namespace Pal.Client.DependencyInjection { if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view")) { - _chatGui.PalError(Localization.Command_pal_stats_CurrentFloor); + _chat.Error(Localization.Command_pal_stats_CurrentFloor); return; } @@ -49,16 +49,16 @@ namespace Pal.Client.DependencyInjection } else { - _chatGui.PalError(Localization.Command_pal_stats_UnableToFetchStatistics); + _chat.Error(Localization.Command_pal_stats_UnableToFetchStatistics); } } catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) { - _chatGui.PalError(Localization.Command_pal_stats_CurrentFloor); + _chat.Error(Localization.Command_pal_stats_CurrentFloor); } catch (Exception e) { - _chatGui.PalError(e.ToString()); + _chat.Error(e.ToString()); } } } diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index bc8df78..8d31745 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -22,6 +22,7 @@ using Pal.Client.Configuration; using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection.Logging; +using Pal.Client.Extensions; using Pal.Client.Net; using Pal.Client.Properties; using Pal.Client.Rendering; @@ -72,6 +73,7 @@ namespace Pal.Client services.AddSingleton(clientState); services.AddSingleton(gameGui); services.AddSingleton(chatGui); + services.AddSingleton(); services.AddSingleton(objectTable); services.AddSingleton(framework); services.AddSingleton(condition); @@ -151,9 +153,11 @@ namespace Pal.Client { using IDisposable? logScope = _logger.BeginScope("AsyncInit"); + Chat? chat = null; try { _logger.LogInformation("Starting async init"); + chat = _serviceProvider.GetService(); // initialize database await using (var scope = _serviceProvider.CreateAsyncScope()) @@ -191,7 +195,7 @@ namespace Pal.Client catch (Exception e) { _logger.LogError(e, "Async load failed"); - chatGui.PrintError($"Async loading failed: {e.GetType()}: {e.Message}"); + chat?.Error($"Async loading failed: {e.GetType()}: {e.Message}"); } }); } diff --git a/Pal.Client/Hooks.cs b/Pal.Client/Hooks.cs index ae197fb..17707e6 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -75,7 +75,7 @@ namespace Pal.Client /* if (Service.Configuration.BetaKey == "VFX") - _chatGui.PalPrint($"{vfxPath} on {obj}"); + _chat.PalPrint($"{vfxPath} on {obj}"); */ if (obj is BattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395)) diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index bc244cf..a62fe1b 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Pal.Client.Extensions; using Pal.Client.Properties; using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client.Net { @@ -68,7 +69,7 @@ namespace Pal.Client.Net _logger.LogError("Account creation failed with error {Error}", createAccountReply.Error); if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) { - _chatGui.PalError(Localization.ConnectionError_OldVersion); + _chat.Error(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error)); @@ -123,7 +124,7 @@ namespace Pal.Client.Net } if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) { - _chatGui.PalError(Localization.ConnectionError_OldVersion); + _chat.Error(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error)); @@ -181,7 +182,7 @@ namespace Pal.Client.Net public bool IsLoggedIn { get; } public string? AuthToken { get; } public JwtClaims? Claims { get; } - public DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; + private DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow; public bool IsValid => IsLoggedIn && !IsExpired; diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index e858308..83b35a3 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using System; using Dalamud.Game.Gui; using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client.Net { @@ -19,7 +20,7 @@ namespace Pal.Client.Net private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; - private readonly ChatGui _chatGui; + private readonly Chat _chat; private readonly ConfigurationManager _configurationManager; private readonly IPalacePalConfiguration _configuration; @@ -30,13 +31,13 @@ namespace Pal.Client.Net public RemoteApi( ILoggerFactory loggerFactory, ILogger logger, - ChatGui chatGui, + Chat chat, ConfigurationManager configurationManager, IPalacePalConfiguration configuration) { _loggerFactory = loggerFactory; _logger = logger; - _chatGui = chatGui; + _chat = chat; _configurationManager = configurationManager; _configuration = configuration; } diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 5286b81..5595b52 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -26,19 +26,20 @@ namespace Pal.Client.Rendering private readonly ILogger _logger; private readonly DebugState _debugState; private readonly ClientState _clientState; - private readonly ChatGui _chatGui; + private readonly Chat _chat; public SplatoonRenderer( ILogger logger, DalamudPluginInterface pluginInterface, IDalamudPlugin dalamudPlugin, DebugState debugState, - ClientState clientState, ChatGui chatGui) + ClientState clientState, + Chat chat) { _logger = logger; _debugState = debugState; _clientState = clientState; - _chatGui = chatGui; + _chat = chat; _logger.LogInformation("Initializing splatoon..."); ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI); @@ -100,6 +101,9 @@ namespace Pal.Client.Rendering return new SplatoonElement(this, element); } + // TODO This should be handled differently + // - make SimpleRenderer implement this + // - return error (if any) instead of using chat here public void DrawDebugItems(Vector4 trapColor, Vector4 hoardColor) { try @@ -117,7 +121,7 @@ namespace Pal.Client.Rendering elements.Cast().Select(x => x.Delegate).ToArray(), new[] { Environment.TickCount64 + 10000 })) { - _chatGui.PalMessage("Could not draw markers :("); + _chat.Message("Could not draw markers :("); } } } @@ -137,9 +141,9 @@ namespace Pal.Client.Rendering string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t); if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon") { - _chatGui.PalError( + _chat.Error( $"Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); - _chatGui.PalMessage( + _chat.Message( "You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); return; } @@ -150,7 +154,7 @@ namespace Pal.Client.Rendering // not relevant } - _chatGui.PalError("Could not draw markers, is Splatoon installed and enabled?"); + _chat.Error("Could not draw markers, is Splatoon installed and enabled?"); } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index fa4cb93..2e35c71 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -31,20 +31,20 @@ namespace Pal.Client.Scheduled internal sealed class Handler : IQueueOnFrameworkThread.Handler { - private readonly ChatGui _chatGui; + private readonly Chat _chat; private readonly FloorService _floorService; private readonly ImportService _importService; private readonly ConfigWindow _configWindow; public Handler( ILogger logger, - ChatGui chatGui, + Chat chat, FloorService floorService, ImportService importService, ConfigWindow configWindow) : base(logger) { - _chatGui = chatGui; + _chat = chat; _floorService = floorService; _importService = importService; _configWindow = configWindow; @@ -88,13 +88,13 @@ namespace Pal.Client.Scheduled _logger.LogInformation( $"Imported {import.ExportId} for {import.ImportedTraps} traps, {import.ImportedHoardCoffers} hoard coffers"); - _chatGui.PalMessage(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, + _chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, import.ImportedHoardCoffers)); } catch (Exception e) { _logger.LogError(e, "Import failed"); - _chatGui.PalError(string.Format(Localization.Error_ImportFailed, e)); + _chat.Error(string.Format(Localization.Error_ImportFailed, e)); } } @@ -104,14 +104,14 @@ namespace Pal.Client.Scheduled { _logger.LogError( "Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}", import.Export.ExportVersion, ExportConfig.ExportVersion); - _chatGui.PalError(Localization.Error_ImportFailed_IncompatibleVersion); + _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); - _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); + _chat.Error(Localization.Error_ImportFailed_InvalidFile); return false; } @@ -121,7 +121,7 @@ namespace Pal.Client.Scheduled { // If we allow for backups as import/export, this should be removed _logger.LogError("Import: No server URL"); - _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); + _chat.Error(Localization.Error_ImportFailed_InvalidFile); return false; } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 84a8889..17368c0 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -41,7 +41,7 @@ namespace Pal.Client.Windows private readonly FrameworkService _frameworkService; private readonly FloorService _floorService; private readonly DebugState _debugState; - private readonly ChatGui _chatGui; + private readonly Chat _chat; private readonly RemoteApi _remoteApi; private readonly ImportService _importService; @@ -74,7 +74,7 @@ namespace Pal.Client.Windows FrameworkService frameworkService, FloorService floorService, DebugState debugState, - ChatGui chatGui, + Chat chat, RemoteApi remoteApi, ImportService importService) : base(WindowId) @@ -88,7 +88,7 @@ namespace Pal.Client.Windows _frameworkService = frameworkService; _floorService = floorService; _debugState = debugState; - _chatGui = chatGui; + _chat = chat; _remoteApi = remoteApi; _importService = importService; @@ -504,17 +504,17 @@ namespace Pal.Client.Windows await using var output = File.Create(destinationPath); export.WriteTo(output); - _chatGui.PalMessage($"Export saved as {destinationPath}."); + _chat.Message($"Export saved as {destinationPath}."); } else { - _chatGui.PalError("Export failed due to server error."); + _chat.Error("Export failed due to server error."); } } catch (Exception e) { _logger.LogError(e, "Export failed"); - _chatGui.PalError($"Export failed: {e}"); + _chat.Error($"Export failed: {e}"); } }); } From 57a5be79383231f7c002f7bbfef70ed0d686e144 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 16:30:20 +0100 Subject: [PATCH 22/38] Config/Db: Move V1 json layouts to legacy folders --- .../Configuration/ConfigurationManager.cs | 1 + Pal.Client/Configuration/ConfigurationV7.cs | 5 - .../{ => Legacy}/ConfigurationV1.cs | 13 +- .../Configuration/Legacy/JsonFloorState.cs | 164 ++++++++++++++++++ Pal.Client/Configuration/Legacy/JsonMarker.cs | 26 +++ Pal.Client/DependencyInjectionContext.cs | 17 +- 6 files changed, 209 insertions(+), 17 deletions(-) rename Pal.Client/Configuration/{ => Legacy}/ConfigurationV1.cs (95%) create mode 100644 Pal.Client/Configuration/Legacy/JsonFloorState.cs create mode 100644 Pal.Client/Configuration/Legacy/JsonMarker.cs diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 4b4db7a..1c28c1d 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -9,6 +9,7 @@ using Dalamud.Plugin; using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using NJson = Newtonsoft.Json; diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs index a6032d6..236fea9 100644 --- a/Pal.Client/Configuration/ConfigurationV7.cs +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json.Serialization; namespace Pal.Client.Configuration { @@ -17,10 +16,6 @@ namespace Pal.Client.Configuration public RendererConfiguration Renderer { get; set; } = new(); public List Accounts { get; set; } = new(); - [JsonIgnore] - [Obsolete] - public List ImportHistory { get; set; } = new(); - public IAccountConfiguration CreateAccount(string server, Guid accountId) { var account = new AccountConfigurationV7(server, accountId); diff --git a/Pal.Client/Configuration/ConfigurationV1.cs b/Pal.Client/Configuration/Legacy/ConfigurationV1.cs similarity index 95% rename from Pal.Client/Configuration/ConfigurationV1.cs rename to Pal.Client/Configuration/Legacy/ConfigurationV1.cs index ef98640..6b94807 100644 --- a/Pal.Client/Configuration/ConfigurationV1.cs +++ b/Pal.Client/Configuration/Legacy/ConfigurationV1.cs @@ -1,6 +1,4 @@ -using Dalamud.Logging; -using Newtonsoft.Json; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -8,8 +6,9 @@ using System.Linq; using System.Numerics; using Dalamud.Plugin; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; -namespace Pal.Client.Configuration +namespace Pal.Client.Configuration.Legacy { [Obsolete] public sealed class ConfigurationV1 @@ -90,7 +89,7 @@ namespace Pal.Client.Configuration // 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; - LocalState.ForEach(s => + JsonFloorState.ForEach(s => { foreach (var marker in s.Markers) marker.SinceVersion = "0.0"; @@ -100,7 +99,7 @@ namespace Pal.Client.Configuration { s.Backup(suffix: "bak"); - s.Markers = new ConcurrentBag(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == Marker.EType.Hoard || m.WasImported)); + s.Markers = new ConcurrentBag(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == JsonMarker.EType.Hoard || m.WasImported)); s.Save(); //changedAnyFile = true; @@ -131,7 +130,7 @@ namespace Pal.Client.Configuration if (Version == 5) { - LocalState.UpdateAll(); + JsonFloorState.UpdateAll(); Version = 6; Save(pluginInterface); diff --git a/Pal.Client/Configuration/Legacy/JsonFloorState.cs b/Pal.Client/Configuration/Legacy/JsonFloorState.cs new file mode 100644 index 0000000..1199b18 --- /dev/null +++ b/Pal.Client/Configuration/Legacy/JsonFloorState.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using Pal.Client.Extensions; +using Pal.Common; + +namespace Pal.Client.Configuration.Legacy +{ + /// + /// 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; + private static EMode _mode = EMode.Online; // might not be true, but this is 'less strict filtering' for migrations + + internal static void SetContextProperties(string pluginConfigDirectory) + { + _pluginConfigDirectory = pluginConfigDirectory; + } + + public uint TerritoryType { get; set; } + public ConcurrentBag Markers { get; set; } = new(); + + public JsonFloorState(uint territoryType) + { + TerritoryType = territoryType; + } + + private void ApplyFilters() + { + if (_mode == EMode.Offline) + Markers = new ConcurrentBag(Markers.Where(x => x.Seen || (x.WasImported && x.Imports.Count > 0))); + else + // ensure old import markers are removed if they are no longer part of a "current" import + // this MAY remove markers the server sent you (and that you haven't seen), but this should be fixed the next time you enter the zone + Markers = new ConcurrentBag(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0)); + } + + public static JsonFloorState? Load(uint 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] == '[') + { + // 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; + } + + 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); + } + } + + 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()) + { + 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 new file mode 100644 index 0000000..06b4607 --- /dev/null +++ b/Pal.Client/Configuration/Legacy/JsonMarker.cs @@ -0,0 +1,26 @@ +using System; +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; } + + public enum EType + { + Unknown = 0, + Trap = 1, + Hoard = 2, + Debug = 3, + } + } +} diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 8d31745..c8e3f0c 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Commands; using Pal.Client.Configuration; +using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection.Logging; @@ -39,7 +40,11 @@ namespace Pal.Client { public static DalamudLoggerProvider LoggerProvider { get; } = new(); + /// + /// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes. + /// private readonly ILogger _logger = LoggerProvider.CreateLogger(); + private readonly string _sqliteConnectionString; private readonly CancellationTokenSource _initCts = new(); private ServiceProvider? _serviceProvider; @@ -56,9 +61,14 @@ namespace Pal.Client CommandManager commandManager, DataManager dataManager) { - // Temporary logger, will be overriden later _logger.LogInformation("Building service container"); + // set up legacy services +#pragma warning disable CS0612 + JsonFloorState.SetContextProperties(pluginInterface.GetPluginConfigDirectory()); +#pragma warning restore CS0612 + + // set up logging CancellationToken token = _initCts.Token; IServiceCollection services = new ServiceCollection(); services.AddLogging(builder => @@ -67,6 +77,7 @@ namespace Pal.Client .AddFilter("Grpc", LogLevel.Debug) .ClearProviders() .AddProvider(LoggerProvider)); + // dalamud services.AddSingleton(this); services.AddSingleton(pluginInterface); @@ -171,10 +182,6 @@ namespace Pal.Client token.ThrowIfCancellationRequested(); - // set up legacy services - LocalState.PluginConfigDirectory = pluginInterface.GetPluginConfigDirectory(); - LocalState.Mode = _serviceProvider.GetRequiredService().Mode; - // windows that have logic to open on startup _serviceProvider.GetRequiredService(); From e624c5b6289ee12ac209a90e673ebf3ee0046826 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 18:36:22 +0100 Subject: [PATCH 23/38] Db: Migrate markers to db --- .../Configuration/ConfigurationManager.cs | 4 +- .../Configuration/Legacy/JsonFloorState.cs | 14 +- .../Configuration/Legacy/JsonMigration.cs | 141 ++++++++++++++++++ Pal.Client/Database/ClientLocation.cs | 39 +++++ Pal.Client/Database/ImportHistory.cs | 3 + ...30217160342_AddClientLocations.Designer.cs | 136 +++++++++++++++++ .../20230217160342_AddClientLocations.cs | 100 +++++++++++++ .../PalClientContextModelSnapshot.cs | 93 +++++++++++- Pal.Client/Database/PalClientContext.cs | 10 ++ Pal.Client/Database/RemoteEncounter.cs | 41 +++++ Pal.Client/DependencyInjectionContext.cs | 9 +- Pal.Client/Pal.Client.csproj | 2 +- Pal.Common/Pal.Common.csproj | 2 +- 13 files changed, 582 insertions(+), 12 deletions(-) create mode 100644 Pal.Client/Configuration/Legacy/JsonMigration.cs create mode 100644 Pal.Client/Database/ClientLocation.cs create mode 100644 Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs create mode 100644 Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs create mode 100644 Pal.Client/Database/RemoteEncounter.cs diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 1c28c1d..f8c584f 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -28,8 +28,6 @@ namespace Pal.Client.Configuration _logger = logger; _pluginInterface = pluginInterface; _serviceProvider = serviceProvider; - - Migrate(); } private string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); @@ -54,7 +52,7 @@ namespace Pal.Client.Configuration #pragma warning disable CS0612 #pragma warning disable CS0618 - private void Migrate() + public void Migrate() { if (_pluginInterface.ConfigFile.Exists) { diff --git a/Pal.Client/Configuration/Legacy/JsonFloorState.cs b/Pal.Client/Configuration/Legacy/JsonFloorState.cs index 1199b18..5742b2f 100644 --- a/Pal.Client/Configuration/Legacy/JsonFloorState.cs +++ b/Pal.Client/Configuration/Legacy/JsonFloorState.cs @@ -18,18 +18,18 @@ namespace Pal.Client.Configuration.Legacy private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; private const int CurrentVersion = 4; - private static string _pluginConfigDirectory; - private static EMode _mode = EMode.Online; // might not be true, but this is 'less strict filtering' for migrations + private static string _pluginConfigDirectory = null!; + private static readonly EMode _mode = EMode.Online; // might not be true, but this is 'less strict filtering' for migrations internal static void SetContextProperties(string pluginConfigDirectory) { _pluginConfigDirectory = pluginConfigDirectory; } - public uint TerritoryType { get; set; } + public ushort TerritoryType { get; set; } public ConcurrentBag Markers { get; set; } = new(); - public JsonFloorState(uint territoryType) + public JsonFloorState(ushort territoryType) { TerritoryType = territoryType; } @@ -44,7 +44,7 @@ namespace Pal.Client.Configuration.Legacy Markers = new ConcurrentBag(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0)); } - public static JsonFloorState? Load(uint territoryType) + public static JsonFloorState? Load(ushort territoryType) { string path = GetSaveLocation(territoryType); if (!File.Exists(path)) @@ -136,6 +136,10 @@ namespace Pal.Client.Configuration.Legacy { 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); diff --git a/Pal.Client/Configuration/Legacy/JsonMigration.cs b/Pal.Client/Configuration/Legacy/JsonMigration.cs new file mode 100644 index 0000000..c73e7ea --- /dev/null +++ b/Pal.Client/Configuration/Legacy/JsonMigration.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Plugin; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +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; + + public JsonMigration(ILogger logger, IServiceScopeFactory serviceScopeFactory, + DalamudPluginInterface pluginInterface) + { + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; + _pluginInterface = pluginInterface; + } + +#pragma warning disable CS0612 + public async Task MigrateAsync(CancellationToken cancellationToken) + { + List floorsToMigrate = new(); + JsonFloorState.ForEach(floorsToMigrate.Add); + + if (floorsToMigrate.Count == 0) + { + _logger.LogInformation("Found no floors to migrate"); + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + 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); + + 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"); + } + + 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(), + }; + + 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/ClientLocation.cs b/Pal.Client/Database/ClientLocation.cs new file mode 100644 index 0000000..ac0714f --- /dev/null +++ b/Pal.Client/Database/ClientLocation.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Pal.Client.Database +{ + 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(); + + public enum EType + { + Trap = 1, + Hoard = 2, + } + } +} diff --git a/Pal.Client/Database/ImportHistory.cs b/Pal.Client/Database/ImportHistory.cs index 33e74f4..535b502 100644 --- a/Pal.Client/Database/ImportHistory.cs +++ b/Pal.Client/Database/ImportHistory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Pal.Client.Database { @@ -8,5 +9,7 @@ namespace Pal.Client.Database 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/Migrations/20230217160342_AddClientLocations.Designer.cs b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs new file mode 100644 index 0000000..a7b24d6 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs @@ -0,0 +1,136 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Pal.Client.Database; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + [DbContext(typeof(PalClientContext))] + [Migration("20230217160342_AddClientLocations")] + partial class AddClientLocations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.Property("ImportedById") + .HasColumnType("TEXT"); + + b.Property("ImportedLocationsLocalId") + .HasColumnType("INTEGER"); + + b.HasKey("ImportedById", "ImportedLocationsLocalId"); + + b.HasIndex("ImportedLocationsLocalId"); + + b.ToTable("LocationImports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Property("LocalId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.Property("TerritoryType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("LocalId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ExportedAt") + .HasColumnType("TEXT"); + + b.Property("ImportedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Imports"); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("TEXT"); + + b.Property("ClientLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClientLocationId"); + + b.ToTable("RemoteEncounters"); + }); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.HasOne("Pal.Client.Database.ImportHistory", null) + .WithMany() + .HasForeignKey("ImportedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Pal.Client.Database.ClientLocation", null) + .WithMany() + .HasForeignKey("ImportedLocationsLocalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") + .WithMany() + .HasForeignKey("ClientLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClientLocation"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs new file mode 100644 index 0000000..7f4580f --- /dev/null +++ b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + /// + public partial class AddClientLocations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Locations", + columns: table => new + { + LocalId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TerritoryType = table.Column(type: "INTEGER", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + X = table.Column(type: "REAL", nullable: false), + Y = table.Column(type: "REAL", nullable: false), + Z = table.Column(type: "REAL", nullable: false), + Seen = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Locations", x => x.LocalId); + }); + + migrationBuilder.CreateTable( + name: "LocationImports", + columns: table => new + { + ImportedById = table.Column(type: "TEXT", nullable: false), + ImportedLocationsLocalId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LocationImports", x => new { x.ImportedById, x.ImportedLocationsLocalId }); + table.ForeignKey( + name: "FK_LocationImports_Imports_ImportedById", + column: x => x.ImportedById, + principalTable: "Imports", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_LocationImports_Locations_ImportedLocationsLocalId", + column: x => x.ImportedLocationsLocalId, + principalTable: "Locations", + principalColumn: "LocalId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RemoteEncounters", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ClientLocationId = table.Column(type: "INTEGER", nullable: false), + AccountId = table.Column(type: "TEXT", maxLength: 13, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RemoteEncounters", x => x.Id); + table.ForeignKey( + name: "FK_RemoteEncounters_Locations_ClientLocationId", + column: x => x.ClientLocationId, + principalTable: "Locations", + principalColumn: "LocalId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_LocationImports_ImportedLocationsLocalId", + table: "LocationImports", + column: "ImportedLocationsLocalId"); + + migrationBuilder.CreateIndex( + name: "IX_RemoteEncounters_ClientLocationId", + table: "RemoteEncounters", + column: "ClientLocationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LocationImports"); + + migrationBuilder.DropTable( + name: "RemoteEncounters"); + + migrationBuilder.DropTable( + name: "Locations"); + } + } +} diff --git a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs index d6be610..e0813c3 100644 --- a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs +++ b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs @@ -17,6 +17,50 @@ namespace Pal.Client.Database.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.Property("ImportedById") + .HasColumnType("TEXT"); + + b.Property("ImportedLocationsLocalId") + .HasColumnType("INTEGER"); + + b.HasKey("ImportedById", "ImportedLocationsLocalId"); + + b.HasIndex("ImportedLocationsLocalId"); + + b.ToTable("LocationImports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Property("LocalId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.Property("TerritoryType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("LocalId"); + + b.ToTable("Locations", (string)null); + }); + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => { b.Property("Id") @@ -34,7 +78,54 @@ namespace Pal.Client.Database.Migrations b.HasKey("Id"); - b.ToTable("Imports"); + b.ToTable("Imports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("TEXT"); + + b.Property("ClientLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClientLocationId"); + + b.ToTable("RemoteEncounters", (string)null); + }); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.HasOne("Pal.Client.Database.ImportHistory", null) + .WithMany() + .HasForeignKey("ImportedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Pal.Client.Database.ClientLocation", null) + .WithMany() + .HasForeignKey("ImportedLocationsLocalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") + .WithMany() + .HasForeignKey("ClientLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClientLocation"); }); #pragma warning restore 612, 618 } diff --git a/Pal.Client/Database/PalClientContext.cs b/Pal.Client/Database/PalClientContext.cs index b1c65e3..8cbd6a6 100644 --- a/Pal.Client/Database/PalClientContext.cs +++ b/Pal.Client/Database/PalClientContext.cs @@ -4,11 +4,21 @@ namespace Pal.Client.Database { 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) { } + + 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 new file mode 100644 index 0000000..0a0f0a1 --- /dev/null +++ b/Pal.Client/Database/RemoteEncounter.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using Pal.Client.Extensions; +using Pal.Client.Net; + +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!; + + /// + /// 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(); + } + } +} diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index c8e3f0c..eec832d 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -96,6 +96,7 @@ namespace Pal.Client _sqliteConnectionString = $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; services.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); + services.AddTransient(); // plugin-specific services.AddSingleton(); @@ -175,13 +176,19 @@ namespace Pal.Client { _logger.LogInformation("Loading database & running migrations"); await using var dbContext = scope.ServiceProvider.GetRequiredService(); - await dbContext.Database.MigrateAsync(); + await dbContext.Database.MigrateAsync(token); _logger.LogInformation("Completed database migrations"); } token.ThrowIfCancellationRequested(); + // v1 migration: config migration for import history, json migration for markers + _serviceProvider.GetRequiredService().Migrate(); + await _serviceProvider.GetRequiredService().MigrateAsync(token); + + token.ThrowIfCancellationRequested(); + // windows that have logic to open on startup _serviceProvider.GetRequiredService(); diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index bb1d9a9..b423f52 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -17,7 +17,7 @@ false true portable - $(SolutionDir)=X:\ + $(SolutionDir)=X:\ diff --git a/Pal.Common/Pal.Common.csproj b/Pal.Common/Pal.Common.csproj index 5166815..720fa9b 100644 --- a/Pal.Common/Pal.Common.csproj +++ b/Pal.Common/Pal.Common.csproj @@ -6,6 +6,6 @@ enable enable portable - $(SolutionDir)=X:\ + $(SolutionDir)=X:\ From 8986b368c7346f527dfc323afafbd3e87718a2f3 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 18:36:55 +0100 Subject: [PATCH 24/38] Log: Use Serilog directly --- .../DependencyInjection/Logging/DalamudLogger.cs | 13 +++++++++---- Pal.Client/DependencyInjectionContext.cs | 5 +++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs b/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs index 1d65fc6..692c311 100644 --- a/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs +++ b/Pal.Client/DependencyInjection/Logging/DalamudLogger.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Text; using Serilog.Events; @@ -24,12 +23,18 @@ namespace Pal.Client.DependencyInjection.Logging where TState : notnull => _scopeProvider?.Push(state) ?? NullScope.Instance; - public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && PluginLogDelegate.IsEnabled(ToSerilogLevel(logLevel)); + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && IsEnabled(ToSerilogLevel(logLevel)); + + private bool IsEnabled(LogEventLevel logEventLevel) => PluginLogDelegate.IsEnabled(logEventLevel); public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (!IsEnabled(logLevel)) + if (logLevel == LogLevel.None) + return; + + LogEventLevel logEventLevel = ToSerilogLevel(logLevel); + if (!IsEnabled(logEventLevel)) return; if (formatter == null) @@ -52,7 +57,7 @@ namespace Pal.Client.DependencyInjection.Logging }, sb); sb.Append(_name).Append(": ").Append(formatter(state, null)); - PluginLogDelegate.Write(ToSerilogLevel(logLevel), exception, sb.ToString()); + PluginLogDelegate.Write(logEventLevel, exception, sb.ToString()); } private LogEventLevel ToSerilogLevel(LogLevel logLevel) diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index eec832d..feaf1a8 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -206,6 +206,11 @@ namespace Pal.Client catch (ObjectDisposedException) { } + catch (TaskCanceledException e) + { + _logger.LogError(e, "Task cancelled"); + chat?.Error("Plugin was unloaded before it finished loading."); + } catch (Exception e) { _logger.LogError(e, "Async load failed"); From 5419f51942bdba7f1ed5bc8238cd5458ec7697c3 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 19:12:44 +0100 Subject: [PATCH 25/38] DI: Only initialize one renderer at once --- Pal.Client/DependencyInjectionContext.cs | 15 ++++-- Pal.Client/Plugin.cs | 20 ++++---- Pal.Client/Rendering/ELayer.cs | 1 + Pal.Client/Rendering/IRenderer.cs | 10 ++-- Pal.Client/Rendering/RenderAdapter.cs | 53 ++++++++++++++++++---- Pal.Client/Rendering/SimpleRenderer.cs | 3 ++ Pal.Client/Rendering/SplatoonRenderer.cs | 11 ++++- Pal.Client/Scheduled/QueuedConfigUpdate.cs | 13 +++++- 8 files changed, 94 insertions(+), 32 deletions(-) diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index feaf1a8..ea2f1ce 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -48,6 +48,7 @@ namespace Pal.Client private readonly string _sqliteConnectionString; private readonly CancellationTokenSource _initCts = new(); private ServiceProvider? _serviceProvider; + private Plugin? _plugin; public string Name => Localization.Palace_Pal; @@ -122,8 +123,8 @@ namespace Pal.Client services.AddSingleton(); // these should maybe be scoped - services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); // queue handling @@ -199,7 +200,7 @@ namespace Pal.Client _serviceProvider.GetRequiredService(); token.ThrowIfCancellationRequested(); - _serviceProvider.GetRequiredService(); + _plugin = new Plugin(pluginInterface, _serviceProvider); _logger.LogInformation("Async init complete"); } @@ -226,15 +227,23 @@ namespace Pal.Client // ensure we're not calling dispose recursively on ourselves if (_serviceProvider != null) { + _logger.LogInformation("Disposing DI Context"); + ServiceProvider serviceProvider = _serviceProvider; _serviceProvider = null; + _plugin?.Dispose(); + _plugin = null; serviceProvider.Dispose(); // ensure we're not keeping the file open longer than the plugin is loaded using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) SqliteConnection.ClearPool(sqliteConnection); } + else + { + _logger.LogDebug("DI context is already disposed"); + } } } } diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index f30ff6b..f8fc54b 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -8,6 +8,7 @@ using System.Linq; using Pal.Client.Properties; using ECommons; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Configuration; namespace Pal.Client @@ -19,24 +20,23 @@ namespace Pal.Client /// internal sealed class Plugin : IDisposable { - private readonly IServiceProvider _serviceProvider; private readonly DalamudPluginInterface _pluginInterface; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; private readonly IPalacePalConfiguration _configuration; private readonly RenderAdapter _renderAdapter; private readonly WindowSystem _windowSystem; public Plugin( - IServiceProvider serviceProvider, DalamudPluginInterface pluginInterface, - IPalacePalConfiguration configuration, - RenderAdapter renderAdapter, - WindowSystem windowSystem) + IServiceProvider serviceProvider) { - _serviceProvider = serviceProvider; _pluginInterface = pluginInterface; - _configuration = configuration; - _renderAdapter = renderAdapter; - _windowSystem = windowSystem; + _serviceProvider = serviceProvider; + _logger = _serviceProvider.GetRequiredService>(); + _configuration = serviceProvider.GetRequiredService(); + _renderAdapter = serviceProvider.GetRequiredService(); + _windowSystem = serviceProvider.GetRequiredService(); LanguageChanged(pluginInterface.UiLanguage); @@ -65,6 +65,8 @@ namespace Pal.Client private void LanguageChanged(string languageCode) { + _logger.LogInformation("Language set to '{Language}'", languageCode); + Localization.Culture = new CultureInfo(languageCode); _windowSystem.Windows.OfType() .Each(w => w.LanguageChanged()); diff --git a/Pal.Client/Rendering/ELayer.cs b/Pal.Client/Rendering/ELayer.cs index be7881e..1027f27 100644 --- a/Pal.Client/Rendering/ELayer.cs +++ b/Pal.Client/Rendering/ELayer.cs @@ -4,5 +4,6 @@ { TrapHoard, RegularCoffers, + Test, } } diff --git a/Pal.Client/Rendering/IRenderer.cs b/Pal.Client/Rendering/IRenderer.cs index 9ecf7d2..68ecf00 100644 --- a/Pal.Client/Rendering/IRenderer.cs +++ b/Pal.Client/Rendering/IRenderer.cs @@ -1,15 +1,13 @@ -using ImGuiNET; -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Numerics; -using System.Text; -using System.Threading.Tasks; +using Pal.Client.Configuration; namespace Pal.Client.Rendering { internal interface IRenderer { + ERenderer GetConfigValue(); + void SetLayer(ELayer layer, IReadOnlyList elements); void ResetLayer(ELayer layer); diff --git a/Pal.Client/Rendering/RenderAdapter.cs b/Pal.Client/Rendering/RenderAdapter.cs index 4afca66..338bb51 100644 --- a/Pal.Client/Rendering/RenderAdapter.cs +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -1,25 +1,55 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Configuration; namespace Pal.Client.Rendering { - internal sealed class RenderAdapter : IRenderer + internal sealed class RenderAdapter : IRenderer, IDisposable { - private readonly SimpleRenderer _simpleRenderer; - private readonly SplatoonRenderer _splatoonRenderer; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; private readonly IPalacePalConfiguration _configuration; - public RenderAdapter(SimpleRenderer simpleRenderer, SplatoonRenderer splatoonRenderer, IPalacePalConfiguration configuration) + private IServiceScope? _renderScope; + + public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger logger, + IPalacePalConfiguration configuration) { - _simpleRenderer = simpleRenderer; - _splatoonRenderer = splatoonRenderer; + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; _configuration = configuration; + + Implementation = Recreate(null); } - public IRenderer Implementation => _configuration.Renderer.SelectedRenderer == ERenderer.Splatoon - ? _splatoonRenderer - : _simpleRenderer; + private IRenderer Recreate(ERenderer? currentRenderer) + { + ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer; + if (targetRenderer == currentRenderer) + return Implementation; + + _renderScope?.Dispose(); + + _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 ConfigUpdated() + { + Implementation = Recreate(Implementation.GetConfigValue()); + } + + public void Dispose() + => _renderScope?.Dispose(); + + public IRenderer Implementation { get; private set; } public void SetLayer(ELayer layer, IReadOnlyList elements) => Implementation.SetLayer(layer, elements); @@ -35,5 +65,8 @@ namespace Pal.Client.Rendering if (Implementation is SimpleRenderer sr) sr.DrawLayers(); } + + public ERenderer GetConfigValue() + => throw new NotImplementedException(); } } diff --git a/Pal.Client/Rendering/SimpleRenderer.cs b/Pal.Client/Rendering/SimpleRenderer.cs index 675426a..14fd0bc 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -146,6 +146,9 @@ namespace Pal.Client.Rendering ImGui.GetWindowDrawList().PathClear(); } + public ERenderer GetConfigValue() + => ERenderer.Simple; + public void Dispose() { foreach (var l in _layers.Values) diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 5595b52..a07d2f5 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -14,6 +14,7 @@ using System.Reflection; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Microsoft.Extensions.Logging; +using Pal.Client.Configuration; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; @@ -41,7 +42,7 @@ namespace Pal.Client.Rendering _clientState = clientState; _chat = chat; - _logger.LogInformation("Initializing splatoon..."); + _logger.LogInformation("Initializing splatoon"); ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI); } @@ -117,7 +118,7 @@ namespace Pal.Client.Rendering CreateElement(Marker.EType.Hoard, pos.Value, ImGui.ColorConvertFloat4ToU32(hoardColor)), }; - if (!Splatoon.AddDynamicElements("PalacePal.Test", + if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), elements.Cast().Select(x => x.Delegate).ToArray(), new[] { Environment.TickCount64 + 10000 })) { @@ -158,12 +159,18 @@ namespace Pal.Client.Rendering } } + 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(); } diff --git a/Pal.Client/Scheduled/QueuedConfigUpdate.cs b/Pal.Client/Scheduled/QueuedConfigUpdate.cs index 3a3e802..d0e71ca 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Rendering; namespace Pal.Client.Scheduled { @@ -11,14 +12,20 @@ namespace Pal.Client.Scheduled private readonly IPalacePalConfiguration _configuration; private readonly FloorService _floorService; private readonly TerritoryState _territoryState; + private readonly RenderAdapter _renderAdapter; - public Handler(ILogger logger, IPalacePalConfiguration configuration, FloorService floorService, - TerritoryState territoryState) + public Handler( + ILogger logger, + IPalacePalConfiguration configuration, + FloorService floorService, + TerritoryState territoryState, + RenderAdapter renderAdapter) : base(logger) { _configuration = configuration; _floorService = floorService; _territoryState = territoryState; + _renderAdapter = renderAdapter; } protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout, ref bool saveMarkers) @@ -33,6 +40,8 @@ namespace Pal.Client.Scheduled recreateLayout = true; saveMarkers = true; } + + _renderAdapter.ConfigUpdated(); } } } From adddbc452c55ffd204adbbabce2d6eaff6a2edd4 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 19:31:43 +0100 Subject: [PATCH 26/38] DI: Support debug items in SimpleRenderer, remove IDrawDebugItems --- .../Properties/Localization.Designer.cs | 10 ------- Pal.Client/Properties/Localization.fr.resx | 3 -- Pal.Client/Properties/Localization.ja.resx | 3 -- Pal.Client/Properties/Localization.resx | 3 -- Pal.Client/Rendering/IDrawDebugItems.cs | 9 ------ Pal.Client/Rendering/IRenderer.cs | 2 ++ Pal.Client/Rendering/RenderAdapter.cs | 30 ++++++++++--------- Pal.Client/Rendering/RenderData.cs | 1 + Pal.Client/Rendering/SimpleRenderer.cs | 27 ++++++++++++++--- Pal.Client/Rendering/SplatoonRenderer.cs | 21 +++++-------- Pal.Client/Windows/ConfigWindow.cs | 7 ++--- 11 files changed, 52 insertions(+), 64 deletions(-) delete mode 100644 Pal.Client/Rendering/IDrawDebugItems.cs diff --git a/Pal.Client/Properties/Localization.Designer.cs b/Pal.Client/Properties/Localization.Designer.cs index f123400..d15a42c 100644 --- a/Pal.Client/Properties/Localization.Designer.cs +++ b/Pal.Client/Properties/Localization.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -385,15 +384,6 @@ namespace Pal.Client.Properties { } } - /// - /// Looks up a localized string similar to Splatoon Test:. - /// - internal static string Config_Splatoon_Test { - get { - return ResourceManager.GetString("Config_Splatoon_Test", resourceCulture); - } - } - /// /// Looks up a localized string similar to Start Export. /// diff --git a/Pal.Client/Properties/Localization.fr.resx b/Pal.Client/Properties/Localization.fr.resx index a213ab4..0712d94 100644 --- a/Pal.Client/Properties/Localization.fr.resx +++ b/Pal.Client/Properties/Localization.fr.resx @@ -228,9 +228,6 @@ Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre l Expérimental - - Test de Splatoon : - Dessiner les marqueurs des pièges et coffres autour de soi To test the Splatoon integration, you can draw markers around yourself. diff --git a/Pal.Client/Properties/Localization.ja.resx b/Pal.Client/Properties/Localization.ja.resx index 051b929..4dd5bbf 100644 --- a/Pal.Client/Properties/Localization.ja.resx +++ b/Pal.Client/Properties/Localization.ja.resx @@ -227,9 +227,6 @@ 試験的機能 - - Splatoonのテスト: - 自分の周りにトラップと宝箱を表示する To test the Splatoon integration, you can draw markers around yourself. diff --git a/Pal.Client/Properties/Localization.resx b/Pal.Client/Properties/Localization.resx index 76cf1da..f9e3625 100644 --- a/Pal.Client/Properties/Localization.resx +++ b/Pal.Client/Properties/Localization.resx @@ -239,9 +239,6 @@ This is not synchronized with other players and not saved between floors/runs. experimental - - Splatoon Test: - Draw trap & coffer circles around self To test the Splatoon integration, you can draw markers around yourself. diff --git a/Pal.Client/Rendering/IDrawDebugItems.cs b/Pal.Client/Rendering/IDrawDebugItems.cs deleted file mode 100644 index e584545..0000000 --- a/Pal.Client/Rendering/IDrawDebugItems.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Numerics; - -namespace Pal.Client.Rendering -{ - internal interface IDrawDebugItems - { - void DrawDebugItems(Vector4 trapColor, Vector4 hoardColor); - } -} diff --git a/Pal.Client/Rendering/IRenderer.cs b/Pal.Client/Rendering/IRenderer.cs index 68ecf00..0ac7fd3 100644 --- a/Pal.Client/Rendering/IRenderer.cs +++ b/Pal.Client/Rendering/IRenderer.cs @@ -13,5 +13,7 @@ namespace Pal.Client.Rendering void ResetLayer(ELayer layer); IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false); + + void DrawDebugItems(uint trapColor, uint hoardColor); } } diff --git a/Pal.Client/Rendering/RenderAdapter.cs b/Pal.Client/Rendering/RenderAdapter.cs index 338bb51..5ccb1a1 100644 --- a/Pal.Client/Rendering/RenderAdapter.cs +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -14,6 +14,7 @@ namespace Pal.Client.Rendering private readonly IPalacePalConfiguration _configuration; private IServiceScope? _renderScope; + private IRenderer _implementation; public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger logger, IPalacePalConfiguration configuration) @@ -22,14 +23,14 @@ namespace Pal.Client.Rendering _logger = logger; _configuration = configuration; - Implementation = Recreate(null); + _implementation = Recreate(null); } private IRenderer Recreate(ERenderer? currentRenderer) { ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer; if (targetRenderer == currentRenderer) - return Implementation; + return _implementation; _renderScope?.Dispose(); @@ -43,30 +44,31 @@ namespace Pal.Client.Rendering public void ConfigUpdated() { - Implementation = Recreate(Implementation.GetConfigValue()); + _implementation = Recreate(_implementation.GetConfigValue()); } public void Dispose() => _renderScope?.Dispose(); - public IRenderer Implementation { get; private set; } - public void SetLayer(ELayer layer, IReadOnlyList elements) - => Implementation.SetLayer(layer, elements); + => _implementation.SetLayer(layer, elements); public void ResetLayer(ELayer layer) - => Implementation.ResetLayer(layer); + => _implementation.ResetLayer(layer); public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) - => Implementation.CreateElement(type, pos, color, fill); - - public void DrawLayers() - { - if (Implementation is SimpleRenderer sr) - sr.DrawLayers(); - } + => _implementation.CreateElement(type, pos, color, fill); 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(); + } } } diff --git a/Pal.Client/Rendering/RenderData.cs b/Pal.Client/Rendering/RenderData.cs index 2c4b802..41c64ed 100644 --- a/Pal.Client/Rendering/RenderData.cs +++ b/Pal.Client/Rendering/RenderData.cs @@ -3,5 +3,6 @@ internal static class RenderData { 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 14fd0bc..d7724f5 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -66,6 +66,22 @@ namespace Pal.Client.Rendering }; } + public void DrawDebugItems(uint trapColor, uint hoardColor) + { + _layers[ELayer.Test] = new SimpleLayer + { + TerritoryType = _clientState.TerritoryType, + Elements = new List + { + (SimpleElement)CreateElement(Marker.EType.Trap, _clientState.LocalPlayer?.Position ?? default, + trapColor), + (SimpleElement)CreateElement(Marker.EType.Hoard, _clientState.LocalPlayer?.Position ?? default, + hoardColor) + }, + ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout + }; + } + public void DrawLayers() { if (_layers.Count == 0) @@ -80,15 +96,14 @@ namespace Pal.Client.Rendering ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.AlwaysUseWindowPadding)) { - ushort territoryType = _clientState.TerritoryType; - - foreach (var layer in _layers.Values.Where(l => l.TerritoryType == territoryType)) + 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.TerritoryType != territoryType).Select(l => l.Key) + foreach (var key in _layers.Where(l => !l.Value.IsValid(_clientState)) + .Select(l => l.Key) .ToList()) ResetLayer(key); @@ -159,6 +174,10 @@ namespace Pal.Client.Rendering { 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() { diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index a07d2f5..82dd7f2 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -1,10 +1,8 @@ -using Dalamud.Logging; -using Dalamud.Plugin; +using Dalamud.Plugin; using ECommons; using ECommons.Reflection; using ECommons.Schedulers; using ECommons.SplatoonAPI; -using ImGuiNET; using System; using System.Collections; using System.Collections.Generic; @@ -12,15 +10,13 @@ using System.Linq; using System.Numerics; using System.Reflection; using Dalamud.Game.ClientState; -using Dalamud.Game.Gui; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; -using Pal.Client.Extensions; namespace Pal.Client.Rendering { - internal sealed class SplatoonRenderer : IRenderer, IDrawDebugItems, IDisposable + internal sealed class SplatoonRenderer : IRenderer, IDisposable { private const long OnTerritoryChange = -2; @@ -102,25 +98,24 @@ namespace Pal.Client.Rendering return new SplatoonElement(this, element); } - // TODO This should be handled differently - // - make SimpleRenderer implement this - // - return error (if any) instead of using chat here - public void DrawDebugItems(Vector4 trapColor, Vector4 hoardColor) + public void DrawDebugItems(uint trapColor, uint hoardColor) { try { Vector3? pos = _clientState.LocalPlayer?.Position; if (pos != null) { + ResetLayer(ELayer.Test); + var elements = new List { - CreateElement(Marker.EType.Trap, pos.Value, ImGui.ColorConvertFloat4ToU32(trapColor)), - CreateElement(Marker.EType.Hoard, pos.Value, ImGui.ColorConvertFloat4ToU32(hoardColor)), + CreateElement(Marker.EType.Trap, pos.Value, trapColor), + CreateElement(Marker.EType.Hoard, pos.Value, hoardColor), }; if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), elements.Cast().Select(x => x.Delegate).ToArray(), - new[] { Environment.TickCount64 + 10000 })) + new[] { Environment.TickCount64 + RenderData.TestLayerTimeout })) { _chat.Message("Could not draw markers :("); } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 17368c0..fed5212 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -363,12 +363,9 @@ namespace Pal.Client.Windows saveAndClose = ImGui.Button(Localization.SaveAndClose); ImGui.Separator(); - ImGui.Text(Localization.Config_Splatoon_Test); - ImGui.BeginDisabled(!(_renderAdapter.Implementation is IDrawDebugItems)); if (ImGui.Button(Localization.Config_Splatoon_DrawCircles)) - (_renderAdapter.Implementation as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, - _hoardConfig.Color); - ImGui.EndDisabled(); + _renderAdapter.DrawDebugItems(ImGui.ColorConvertFloat4ToU32(_trapConfig.Color), + ImGui.ColorConvertFloat4ToU32(_hoardConfig.Color)); ImGui.EndTabItem(); } From f63e70b0c4ba481d862bb73f687c823208d38fb8 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 18 Feb 2023 04:34:49 +0100 Subject: [PATCH 27/38] DI: Build root scope async while still registering events/commands during plugin init --- Pal.Client/Commands/PalCommand.cs | 141 ----------------- Pal.Client/Commands/PalConfigCommand.cs | 31 ++++ Pal.Client/Commands/PalNearCommand.cs | 67 ++++++++ Pal.Client/Commands/PalStatsCommand.cs | 18 +++ .../Commands/PalTestConnectionCommand.cs | 20 +++ Pal.Client/DependencyInjectionContext.cs | 102 ++++-------- Pal.Client/DependencyInjectionLoader.cs | 107 +++++++++++++ Pal.Client/Plugin.cs | 147 ++++++++++++++---- .../Properties/Localization.Designer.cs | 18 +-- Pal.Client/Properties/Localization.fr.resx | 4 - Pal.Client/Properties/Localization.ja.resx | 4 - Pal.Client/Properties/Localization.resx | 10 +- 12 files changed, 401 insertions(+), 268 deletions(-) delete mode 100644 Pal.Client/Commands/PalCommand.cs create mode 100644 Pal.Client/Commands/PalConfigCommand.cs create mode 100644 Pal.Client/Commands/PalNearCommand.cs create mode 100644 Pal.Client/Commands/PalStatsCommand.cs create mode 100644 Pal.Client/Commands/PalTestConnectionCommand.cs create mode 100644 Pal.Client/DependencyInjectionLoader.cs diff --git a/Pal.Client/Commands/PalCommand.cs b/Pal.Client/Commands/PalCommand.cs deleted file mode 100644 index bd20305..0000000 --- a/Pal.Client/Commands/PalCommand.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Linq; -using Dalamud.Game.ClientState; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using ECommons.Schedulers; -using Pal.Client.Configuration; -using Pal.Client.DependencyInjection; -using Pal.Client.Extensions; -using Pal.Client.Properties; -using Pal.Client.Rendering; -using Pal.Client.Windows; - -namespace Pal.Client.Commands -{ - // should restructure this when more commands exist, if that ever happens - // this command is more-or-less a debug/troubleshooting command, if anything - internal sealed class PalCommand : IDisposable - { - private readonly IPalacePalConfiguration _configuration; - private readonly CommandManager _commandManager; - private readonly Chat _chat; - private readonly StatisticsService _statisticsService; - private readonly ConfigWindow _configWindow; - private readonly TerritoryState _territoryState; - private readonly FloorService _floorService; - private readonly ClientState _clientState; - - public PalCommand( - IPalacePalConfiguration configuration, - CommandManager commandManager, - Chat chat, - StatisticsService statisticsService, - ConfigWindow configWindow, - TerritoryState territoryState, - FloorService floorService, - ClientState clientState) - { - _configuration = configuration; - _commandManager = commandManager; - _chat = chat; - _statisticsService = statisticsService; - _configWindow = configWindow; - _territoryState = territoryState; - _floorService = floorService; - _clientState = clientState; - - _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) - { - HelpMessage = Localization.Command_pal_HelpText - }); - } - - public void Dispose() - { - _commandManager.RemoveHandler("/pal"); - } - - private void OnCommand(string command, string arguments) - { - if (_configuration.FirstUse) - { - _chat.Error(Localization.Error_FirstTimeSetupRequired); - return; - } - - try - { - arguments = arguments.Trim(); - switch (arguments) - { - case "stats": - _statisticsService.ShowGlobalStatistics(); - break; - - case "test-connection": - case "tc": - _configWindow.IsOpen = true; - var _ = new TickScheduler(() => _configWindow.TestConnection()); - break; - -#if DEBUG - case "update-saves": - LocalState.UpdateAll(); - _chat.Message(Localization.Command_pal_updatesaves); - break; -#endif - - case "": - case "config": - _configWindow.Toggle(); - break; - - case "near": - DebugNearest(_ => true); - break; - - case "tnear": - DebugNearest(m => m.Type == Marker.EType.Trap); - break; - - case "hnear": - DebugNearest(m => m.Type == Marker.EType.Hoard); - break; - - default: - _chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, - command)); - break; - } - } - catch (Exception e) - { - _chat.Error(e.ToString()); - } - } - - private void DebugNearest(Predicate predicate) - { - if (!_territoryState.IsInDeepDungeon()) - return; - - var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); - var playerPosition = _clientState.LocalPlayer?.Position; - if (playerPosition == null) - return; - _chat.Message($"{playerPosition}"); - - var nearbyMarkers = state.Markers - .Where(m => predicate(m)) - .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) - .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) - .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/PalConfigCommand.cs b/Pal.Client/Commands/PalConfigCommand.cs new file mode 100644 index 0000000..296392f --- /dev/null +++ b/Pal.Client/Commands/PalConfigCommand.cs @@ -0,0 +1,31 @@ +using Dalamud.Interface.Windowing; +using Pal.Client.Configuration; +using Pal.Client.Windows; + +namespace Pal.Client.Commands +{ + internal class PalConfigCommand + { + private readonly IPalacePalConfiguration _configuration; + private readonly AgreementWindow _agreementWindow; + private readonly ConfigWindow _configWindow; + + public PalConfigCommand( + IPalacePalConfiguration configuration, + AgreementWindow agreementWindow, + ConfigWindow configWindow) + { + _configuration = configuration; + _agreementWindow = agreementWindow; + _configWindow = configWindow; + } + + 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 new file mode 100644 index 0000000..b24c959 --- /dev/null +++ b/Pal.Client/Commands/PalNearCommand.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using Dalamud.Game.ClientState; +using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; +using Pal.Client.Rendering; + +namespace Pal.Client.Commands +{ + internal sealed class PalNearCommand + { + 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) + { + _chat = chat; + _clientState = clientState; + _territoryState = territoryState; + _floorService = floorService; + } + + public void Execute(string arguments) + { + switch (arguments) + { + default: + DebugNearest(_ => true); + break; + + case "tnear": + DebugNearest(m => m.Type == Marker.EType.Trap); + break; + + case "hnear": + DebugNearest(m => m.Type == Marker.EType.Hoard); + break; + } + } + + private void DebugNearest(Predicate predicate) + { + if (!_territoryState.IsInDeepDungeon()) + return; + + var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); + var playerPosition = _clientState.LocalPlayer?.Position; + if (playerPosition == null) + return; + _chat.Message($"{playerPosition}"); + + var nearbyMarkers = state.Markers + .Where(m => predicate(m)) + .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) + .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) + .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 new file mode 100644 index 0000000..917efcc --- /dev/null +++ b/Pal.Client/Commands/PalStatsCommand.cs @@ -0,0 +1,18 @@ +using System; +using Pal.Client.DependencyInjection; + +namespace Pal.Client.Commands +{ + internal sealed class PalStatsCommand + { + private readonly StatisticsService _statisticsService; + + public PalStatsCommand(StatisticsService statisticsService) + { + _statisticsService = statisticsService; + } + + public void Execute() + => _statisticsService.ShowGlobalStatistics(); + } +} diff --git a/Pal.Client/Commands/PalTestConnectionCommand.cs b/Pal.Client/Commands/PalTestConnectionCommand.cs new file mode 100644 index 0000000..174330c --- /dev/null +++ b/Pal.Client/Commands/PalTestConnectionCommand.cs @@ -0,0 +1,20 @@ +using ECommons.Schedulers; +using Pal.Client.Windows; + +namespace Pal.Client.Commands +{ + internal sealed class PalTestConnectionCommand + { + private readonly ConfigWindow _configWindow; + + public PalTestConnectionCommand(ConfigWindow configWindow) + { + _configWindow = configWindow; + } + + public void Execute() + { + var _ = new TickScheduler(() => _configWindow.TestConnection()); + } + } +} diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index ea2f1ce..3ef0b58 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -52,7 +52,8 @@ namespace Pal.Client public string Name => Localization.Palace_Pal; - public DependencyInjectionContext(DalamudPluginInterface pluginInterface, + public DependencyInjectionContext( + DalamudPluginInterface pluginInterface, ClientState clientState, GameGui gameGui, ChatGui chatGui, @@ -70,7 +71,6 @@ namespace Pal.Client #pragma warning restore CS0612 // set up logging - CancellationToken token = _initCts.Token; IServiceCollection services = new ServiceCollection(); services.AddLogging(builder => builder.AddFilter("Pal", LogLevel.Trace) @@ -100,32 +100,37 @@ namespace Pal.Client services.AddTransient(); // plugin-specific - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService().Load()); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService().Load()); services.AddTransient(); - services.AddSingleton(); + + // commands + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // territory & marker related services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // windows & related services - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // these should maybe be scoped services.AddScoped(); services.AddScoped(); - services.AddSingleton(); + services.AddScoped(); // queue handling services.AddTransient, QueuedImport.Handler>(); @@ -161,63 +166,8 @@ namespace Pal.Client // 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, triggering async init"); - Task.Run(async () => - { - using IDisposable? logScope = _logger.BeginScope("AsyncInit"); - - Chat? chat = null; - try - { - _logger.LogInformation("Starting async init"); - chat = _serviceProvider.GetService(); - - // initialize database - await using (var scope = _serviceProvider.CreateAsyncScope()) - { - _logger.LogInformation("Loading database & running migrations"); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); - await dbContext.Database.MigrateAsync(token); - - _logger.LogInformation("Completed database migrations"); - } - - token.ThrowIfCancellationRequested(); - - // v1 migration: config migration for import history, json migration for markers - _serviceProvider.GetRequiredService().Migrate(); - await _serviceProvider.GetRequiredService().MigrateAsync(token); - - token.ThrowIfCancellationRequested(); - - // 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(); - _serviceProvider.GetRequiredService(); - - token.ThrowIfCancellationRequested(); - _plugin = new Plugin(pluginInterface, _serviceProvider); - - _logger.LogInformation("Async init complete"); - } - catch (ObjectDisposedException) - { - } - catch (TaskCanceledException e) - { - _logger.LogError(e, "Task cancelled"); - chat?.Error("Plugin was unloaded before it finished loading."); - } - catch (Exception e) - { - _logger.LogError(e, "Async load failed"); - chat?.Error($"Async loading failed: {e.GetType()}: {e.Message}"); - } - }); + _logger.LogInformation("Service container built, creating plugin"); + _plugin = new Plugin(pluginInterface, _serviceProvider, _initCts.Token); } public void Dispose() diff --git a/Pal.Client/DependencyInjectionLoader.cs b/Pal.Client/DependencyInjectionLoader.cs new file mode 100644 index 0000000..fce371c --- /dev/null +++ b/Pal.Client/DependencyInjectionLoader.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Pal.Client.Commands; +using Pal.Client.Configuration; +using Pal.Client.Configuration.Legacy; +using Pal.Client.Database; +using Pal.Client.DependencyInjection; +using Pal.Client.Properties; +using Pal.Client.Windows; + +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 DependencyInjectionLoader + { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public DependencyInjectionLoader(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public ELoadState LoadState { get; private set; } = ELoadState.Initializing; + + public event Action? InitCompleted; + + public async Task InitializeAsync(CancellationToken cancellationToken) + { + using IDisposable? logScope = _logger.BeginScope("AsyncInit"); + + Chat? chat = null; + try + { + _logger.LogInformation("Starting async init"); + chat = _serviceProvider.GetService(); + + // initialize database + 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"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // v1 migration: config migration for import history, json migration for markers + _serviceProvider.GetRequiredService().Migrate(); + await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + // 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(); + + // eager load any commands to find errors now, not when running them + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + + cancellationToken.ThrowIfCancellationRequested(); + + LoadState = ELoadState.Loaded; + InitCompleted?.Invoke(null); + _logger.LogInformation("Async init complete"); + } + catch (ObjectDisposedException) + { + InitCompleted?.Invoke(null); + LoadState = ELoadState.Error; + } + catch (Exception e) + { + _logger.LogError(e, "Async load failed"); + InitCompleted?.Invoke(() => chat?.Error(string.Format(Localization.Error_LoadFailed, $"{e.GetType()} - {e.Message}"))); + + LoadState = ELoadState.Error; + } + } + + public enum ELoadState + { + Initializing, + Loaded, + Error + } + } +} diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index f8fc54b..62c5adf 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -5,11 +5,17 @@ using Pal.Client.Windows; using System; using System.Globalization; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game.ClientState; +using Dalamud.Game.Command; using Pal.Client.Properties; using ECommons; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Pal.Client.Commands; using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client { @@ -17,51 +23,120 @@ 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 : IDisposable { private readonly DalamudPluginInterface _pluginInterface; - private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private readonly IPalacePalConfiguration _configuration; - private readonly RenderAdapter _renderAdapter; + private readonly CommandManager _commandManager; + private readonly Chat _chat; private readonly WindowSystem _windowSystem; + private readonly ClientState _clientState; + + private readonly IServiceScope _rootScope; + private readonly DependencyInjectionLoader _loader; + + private Action? _loginAction = null; public Plugin( DalamudPluginInterface pluginInterface, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + CancellationToken cancellationToken) { _pluginInterface = pluginInterface; - _serviceProvider = serviceProvider; - _logger = _serviceProvider.GetRequiredService>(); - _configuration = serviceProvider.GetRequiredService(); - _renderAdapter = serviceProvider.GetRequiredService(); + _logger = serviceProvider.GetRequiredService>(); + _commandManager = serviceProvider.GetRequiredService(); + _chat = serviceProvider.GetRequiredService(); _windowSystem = serviceProvider.GetRequiredService(); + _clientState = serviceProvider.GetRequiredService(); - LanguageChanged(pluginInterface.UiLanguage); + _rootScope = serviceProvider.CreateScope(); + _loader = _rootScope.ServiceProvider.GetRequiredService(); + _loader.InitCompleted += InitCompleted; + var _ = Task.Run(async () => await _loader.InitializeAsync(cancellationToken)); pluginInterface.UiBuilder.Draw += Draw; pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; pluginInterface.LanguageChanged += LanguageChanged; + _clientState.Login += Login; + + _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) + { + HelpMessage = Localization.Command_pal_HelpText + }); + } + + private void InitCompleted(Action? loginAction) + { + LanguageChanged(_pluginInterface.UiLanguage); + + 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(); + + IPalacePalConfiguration configuration = + _rootScope.ServiceProvider.GetRequiredService(); + if (configuration.FirstUse && arguments != "" && arguments != "config") + { + _chat.Error(Localization.Error_FirstTimeSetupRequired); + return; + } + + try + { + var sp = _rootScope.ServiceProvider; + + switch (arguments) + { + case "": + case "config": + sp.GetRequiredService().Execute(); + break; + + case "stats": + sp.GetRequiredService().Execute(); + break; + + case "tc": + case "test-connection": + sp.GetRequiredService().Execute(); + break; + + case "near": + case "tnear": + case "hnear": + sp.GetRequiredService().Execute(arguments); + break; + + default: + _chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, + command)); + break; + } + } + catch (Exception e) + { + _chat.Error(e.ToString()); + } } private void OpenConfigUi() - { - Window configWindow; - if (_configuration.FirstUse) - configWindow = _serviceProvider.GetRequiredService(); - else - configWindow = _serviceProvider.GetRequiredService(); - - configWindow.IsOpen = true; - } - - public void Dispose() - { - _pluginInterface.UiBuilder.Draw -= Draw; - _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; - _pluginInterface.LanguageChanged -= LanguageChanged; - } + => _rootScope.ServiceProvider.GetRequiredService().Execute(); private void LanguageChanged(string languageCode) { @@ -74,8 +149,24 @@ namespace Pal.Client private void Draw() { - _renderAdapter.DrawLayers(); - _windowSystem.Draw(); + if (_loader.LoadState == DependencyInjectionLoader.ELoadState.Loaded) + { + _rootScope.ServiceProvider.GetRequiredService().DrawLayers(); + _windowSystem.Draw(); + } + } + + public void Dispose() + { + _commandManager.RemoveHandler("/pal"); + + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; + _pluginInterface.LanguageChanged -= LanguageChanged; + _clientState.Login -= Login; + + _loader.InitCompleted -= InitCompleted; + _rootScope.Dispose(); } } } diff --git a/Pal.Client/Properties/Localization.Designer.cs b/Pal.Client/Properties/Localization.Designer.cs index d15a42c..2c95215 100644 --- a/Pal.Client/Properties/Localization.Designer.cs +++ b/Pal.Client/Properties/Localization.Designer.cs @@ -149,15 +149,6 @@ namespace Pal.Client.Properties { } } - /// - /// Looks up a localized string similar to Updated all locally cached marker files to latest version.. - /// - internal static string Command_pal_updatesaves { - get { - return ResourceManager.GetString("Command_pal_updatesaves", resourceCulture); - } - } - /// /// Looks up a localized string similar to You are NOT in a deep dungeon.. /// @@ -664,6 +655,15 @@ namespace Pal.Client.Properties { } } + /// + /// Looks up a localized string similar to Plugin could not be loaded: {0}. + /// + internal static string Error_LoadFailed { + get { + return ResourceManager.GetString("Error_LoadFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please install this plugin from the official repository at {0} to continue using it.. /// diff --git a/Pal.Client/Properties/Localization.fr.resx b/Pal.Client/Properties/Localization.fr.resx index 0712d94..c634de2 100644 --- a/Pal.Client/Properties/Localization.fr.resx +++ b/Pal.Client/Properties/Localization.fr.resx @@ -61,10 +61,6 @@ Impossible de récupérer les statistiques. Shown when /pal stats produces a server-side error, and the statistics window can't be loaded. - - Mise à jour de tous les marqueurs du cache local vers la dernière version. - Shown after /pal update-saves was successful. - Connexion réussie. diff --git a/Pal.Client/Properties/Localization.ja.resx b/Pal.Client/Properties/Localization.ja.resx index 4dd5bbf..4c72606 100644 --- a/Pal.Client/Properties/Localization.ja.resx +++ b/Pal.Client/Properties/Localization.ja.resx @@ -61,10 +61,6 @@ 統計情報を取得できません。 Shown when /pal stats produces a server-side error, and the statistics window can't be loaded. - - 保存されたマーカーファイルを更新しました。 - Shown after /pal update-saves was successful. - 接続に成功しました。 diff --git a/Pal.Client/Properties/Localization.resx b/Pal.Client/Properties/Localization.resx index f9e3625..ddac1c3 100644 --- a/Pal.Client/Properties/Localization.resx +++ b/Pal.Client/Properties/Localization.resx @@ -46,6 +46,9 @@ Please finish the initial setup first. Before using any /pal command, the initial setup/agreeement needs to be completed. + + Plugin could not be loaded: {0} + Please install this plugin from the official repository at {0} to continue using it. @@ -66,11 +69,7 @@ Unable to fetch statistics. Shown when /pal stats produces a server-side error, and the statistics window can't be loaded. - - Updated all locally cached marker files to latest version. - Shown after /pal update-saves was successful. - - + Connection successful. @@ -322,6 +321,5 @@ This is not synchronized with other players and not saved between floors/runs. Import failed: Invalid file. - From 94f3fa2edef9d04788dda08a3c237a9bd055e065 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 18 Feb 2023 21:12:36 +0100 Subject: [PATCH 28/38] Db: Move Markers into database --- Pal.Client/Commands/PalNearCommand.cs | 16 +- .../Configuration/Legacy/JsonMigration.cs | 3 + Pal.Client/Database/ClientLocation.cs | 11 + ...ndSinceVersionToClientLocation.Designer.cs | 148 ++++++++ ...ImportedAndSinceVersionToClientLocation.cs | 40 +++ .../PalClientContextModelSnapshot.cs | 20 +- .../DependencyInjection/FloorService.cs | 15 - .../DependencyInjection/FrameworkService.cs | 329 +++++++++--------- .../DependencyInjection/ImportService.cs | 59 +++- .../DependencyInjection/TerritoryState.cs | 2 +- Pal.Client/DependencyInjectionContext.cs | 4 +- Pal.Client/Floors/EphemeralLocation.cs | 24 ++ Pal.Client/Floors/FloorService.cs | 147 ++++++++ Pal.Client/Floors/MemoryLocation.cs | 56 +++ Pal.Client/Floors/MemoryTerritory.cs | 49 +++ Pal.Client/Floors/PersistentLocation.cs | 48 +++ Pal.Client/Floors/Tasks/DbTask.cs | 29 ++ Pal.Client/Floors/Tasks/LoadTerritory.cs | 59 ++++ Pal.Client/Floors/Tasks/MarkAsSeen.cs | 33 ++ Pal.Client/Floors/Tasks/MarkRemoteSeen.cs | 39 +++ Pal.Client/Floors/Tasks/SaveNewLocations.cs | 69 ++++ Pal.Client/LocalState.cs | 159 --------- Pal.Client/Marker.cs | 110 ------ Pal.Client/Net/RemoteApi.PalaceService.cs | 30 +- Pal.Client/Net/RemoteApi.cs | 2 +- Pal.Client/Rendering/IRenderer.cs | 3 +- Pal.Client/Rendering/MarkerConfig.cs | 13 +- Pal.Client/Rendering/RenderAdapter.cs | 3 +- Pal.Client/Rendering/SimpleRenderer.cs | 19 +- Pal.Client/Rendering/SplatoonRenderer.cs | 10 +- .../Scheduled/IQueueOnFrameworkThread.cs | 8 +- Pal.Client/Scheduled/QueuedConfigUpdate.cs | 24 +- Pal.Client/Scheduled/QueuedImport.cs | 68 +--- Pal.Client/Scheduled/QueuedSyncResponse.cs | 60 +++- Pal.Client/Scheduled/QueuedUndoImport.cs | 15 +- Pal.Client/Windows/ConfigWindow.cs | 10 +- 36 files changed, 1128 insertions(+), 606 deletions(-) create mode 100644 Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs create mode 100644 Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.cs delete mode 100644 Pal.Client/DependencyInjection/FloorService.cs create mode 100644 Pal.Client/Floors/EphemeralLocation.cs create mode 100644 Pal.Client/Floors/FloorService.cs create mode 100644 Pal.Client/Floors/MemoryLocation.cs create mode 100644 Pal.Client/Floors/MemoryTerritory.cs create mode 100644 Pal.Client/Floors/PersistentLocation.cs create mode 100644 Pal.Client/Floors/Tasks/DbTask.cs create mode 100644 Pal.Client/Floors/Tasks/LoadTerritory.cs create mode 100644 Pal.Client/Floors/Tasks/MarkAsSeen.cs create mode 100644 Pal.Client/Floors/Tasks/MarkRemoteSeen.cs create mode 100644 Pal.Client/Floors/Tasks/SaveNewLocations.cs delete mode 100644 Pal.Client/LocalState.cs delete mode 100644 Pal.Client/Marker.cs diff --git a/Pal.Client/Commands/PalNearCommand.cs b/Pal.Client/Commands/PalNearCommand.cs index b24c959..53e9b8a 100644 --- a/Pal.Client/Commands/PalNearCommand.cs +++ b/Pal.Client/Commands/PalNearCommand.cs @@ -3,6 +3,7 @@ using System.Linq; using Dalamud.Game.ClientState; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Rendering; namespace Pal.Client.Commands @@ -32,30 +33,33 @@ namespace Pal.Client.Commands break; case "tnear": - DebugNearest(m => m.Type == Marker.EType.Trap); + DebugNearest(m => m.Type == MemoryLocation.EType.Trap); break; case "hnear": - DebugNearest(m => m.Type == Marker.EType.Hoard); + DebugNearest(m => m.Type == MemoryLocation.EType.Hoard); break; } } - private void DebugNearest(Predicate predicate) + private void DebugNearest(Predicate predicate) { if (!_territoryState.IsInDeepDungeon()) return; - var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); + var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType); + if (state == null) + return; + var playerPosition = _clientState.LocalPlayer?.Position; if (playerPosition == null) return; _chat.Message($"{playerPosition}"); - var nearbyMarkers = state.Markers + var nearbyMarkers = state.Locations .Where(m => predicate(m)) .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) - .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) + .Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() }) .OrderBy(m => m.distance) .Take(5) .ToList(); diff --git a/Pal.Client/Configuration/Legacy/JsonMigration.cs b/Pal.Client/Configuration/Legacy/JsonMigration.cs index c73e7ea..c657043 100644 --- a/Pal.Client/Configuration/Legacy/JsonMigration.cs +++ b/Pal.Client/Configuration/Legacy/JsonMigration.cs @@ -114,6 +114,9 @@ namespace Pal.Client.Configuration.Legacy .Cast() .Distinct() .ToList(), + + Imported = o.WasImported, + SinceVersion = o.SinceVersion ?? "0.0", }; clientLocation.RemoteEncounters = o.RemoteSeenOn diff --git a/Pal.Client/Database/ClientLocation.cs b/Pal.Client/Database/ClientLocation.cs index ac0714f..e545edd 100644 --- a/Pal.Client/Database/ClientLocation.cs +++ b/Pal.Client/Database/ClientLocation.cs @@ -30,6 +30,17 @@ namespace Pal.Client.Database /// public List ImportedBy { get; set; } = new(); + /// + /// Whether this location was originally imported. + /// + public bool Imported { 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, diff --git a/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs new file mode 100644 index 0000000..24dd296 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs @@ -0,0 +1,148 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Pal.Client.Database; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + [DbContext(typeof(PalClientContext))] + [Migration("20230218112804_AddImportedAndSinceVersionToClientLocation")] + partial class AddImportedAndSinceVersionToClientLocation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.Property("ImportedById") + .HasColumnType("TEXT"); + + b.Property("ImportedLocationsLocalId") + .HasColumnType("INTEGER"); + + b.HasKey("ImportedById", "ImportedLocationsLocalId"); + + b.HasIndex("ImportedLocationsLocalId"); + + b.ToTable("LocationImports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Property("LocalId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Imported") + .HasColumnType("INTEGER"); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.Property("SinceVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TerritoryType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("LocalId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ExportedAt") + .HasColumnType("TEXT"); + + b.Property("ImportedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Imports"); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("TEXT"); + + b.Property("ClientLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClientLocationId"); + + b.ToTable("RemoteEncounters"); + }); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.HasOne("Pal.Client.Database.ImportHistory", null) + .WithMany() + .HasForeignKey("ImportedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Pal.Client.Database.ClientLocation", null) + .WithMany() + .HasForeignKey("ImportedLocationsLocalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") + .WithMany("RemoteEncounters") + .HasForeignKey("ClientLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClientLocation"); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Navigation("RemoteEncounters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.cs b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.cs new file mode 100644 index 0000000..130be35 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + /// + public partial class AddImportedAndSinceVersionToClientLocation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Imported", + table: "Locations", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SinceVersion", + table: "Locations", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Imported", + table: "Locations"); + + migrationBuilder.DropColumn( + name: "SinceVersion", + table: "Locations"); + } + } +} diff --git a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs index e0813c3..963f393 100644 --- a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs +++ b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs @@ -38,9 +38,16 @@ namespace Pal.Client.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Imported") + .HasColumnType("INTEGER"); + b.Property("Seen") .HasColumnType("INTEGER"); + b.Property("SinceVersion") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("TerritoryType") .HasColumnType("INTEGER"); @@ -58,7 +65,7 @@ namespace Pal.Client.Database.Migrations b.HasKey("LocalId"); - b.ToTable("Locations", (string)null); + b.ToTable("Locations"); }); modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => @@ -78,7 +85,7 @@ namespace Pal.Client.Database.Migrations b.HasKey("Id"); - b.ToTable("Imports", (string)null); + b.ToTable("Imports"); }); modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => @@ -99,7 +106,7 @@ namespace Pal.Client.Database.Migrations b.HasIndex("ClientLocationId"); - b.ToTable("RemoteEncounters", (string)null); + b.ToTable("RemoteEncounters"); }); modelBuilder.Entity("ClientLocationImportHistory", b => @@ -120,13 +127,18 @@ namespace Pal.Client.Database.Migrations modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => { b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") - .WithMany() + .WithMany("RemoteEncounters") .HasForeignKey("ClientLocationId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("ClientLocation"); }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Navigation("RemoteEncounters"); + }); #pragma warning restore 612, 618 } } diff --git a/Pal.Client/DependencyInjection/FloorService.cs b/Pal.Client/DependencyInjection/FloorService.cs deleted file mode 100644 index 8cdcb98..0000000 --- a/Pal.Client/DependencyInjection/FloorService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Concurrent; - -namespace Pal.Client.DependencyInjection -{ - internal sealed class FloorService - { - public ConcurrentDictionary FloorMarkers { get; } = new(); - public ConcurrentBag EphemeralMarkers { get; set; } = new(); - - public LocalState GetFloorMarkers(ushort territoryType) - { - return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt)); - } - } -} diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs index 8db9d10..c501410 100644 --- a/Pal.Client/DependencyInjection/FrameworkService.cs +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -14,9 +14,11 @@ using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using Pal.Client.Configuration; using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Net; using Pal.Client.Rendering; using Pal.Client.Scheduled; +using Pal.Common; namespace Pal.Client.DependencyInjection { @@ -84,44 +86,45 @@ namespace Pal.Client.DependencyInjection try { bool recreateLayout = false; - bool saveMarkers = false; while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - HandleQueued(queued, ref recreateLayout, ref saveMarkers); + HandleQueued(queued, ref recreateLayout); if (_territoryState.LastTerritory != _clientState.TerritoryType) { _territoryState.LastTerritory = _clientState.TerritoryType; - _territoryState.TerritorySyncState = SyncState.NotAttempted; + _territoryState.TerritorySyncState = ESyncState.NotAttempted; NextUpdateObjects.Clear(); - if (_territoryState.IsInDeepDungeon()) - _floorService.GetFloorMarkers(_territoryState.LastTerritory); - _floorService.EphemeralMarkers.Clear(); + _floorService.ChangeTerritory(_territoryState.LastTerritory); _territoryState.PomanderOfSight = PomanderState.Inactive; _territoryState.PomanderOfIntuition = PomanderState.Inactive; recreateLayout = true; _debugState.Reset(); } - if (!_territoryState.IsInDeepDungeon()) + if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory)) return; - if (_configuration.Mode == EMode.Online && _territoryState.TerritorySyncState == SyncState.NotAttempted) + if (_configuration.Mode == EMode.Online && + _territoryState.TerritorySyncState == ESyncState.NotAttempted) { - _territoryState.TerritorySyncState = SyncState.Started; + _territoryState.TerritorySyncState = ESyncState.Started; Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory)); } while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - HandleQueued(queued, ref recreateLayout, ref saveMarkers); + HandleQueued(queued, ref recreateLayout); - var currentFloor = _floorService.GetFloorMarkers(_territoryState.LastTerritory); + (IReadOnlyList visiblePersistentMarkers, + IReadOnlyList visibleEphemeralMarkers) = + GetRelevantGameObjects(); - IList visibleMarkers = GetRelevantGameObjects(); - HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, - recreateLayout); - HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); + ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory; + HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout); + + if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout)) + RecreateEphemeralLayout(); } catch (Exception e) { @@ -131,183 +134,161 @@ namespace Pal.Client.DependencyInjection #region Render Markers - private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, + private void HandlePersistentLocations(ETerritoryType territoryType, + IReadOnlyList visiblePersistentMarkers, bool recreateLayout) { - var currentFloorMarkers = currentFloor.Markers; - - bool updateSeenMarkers = false; - var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - foreach (var visibleMarker in visibleMarkers) + bool recreatePersistentLocations = _floorService.MergePersistentLocations( + territoryType, + visiblePersistentMarkers, + recreateLayout, + out List locationsToSync); + recreatePersistentLocations |= CheckLocationsForPomanders(visiblePersistentMarkers); + if (locationsToSync.Count > 0) { - Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); - if (knownMarker != null) - { - if (!knownMarker.Seen) - { - knownMarker.Seen = true; - saveMarkers = true; - } - - // This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, - // markers discovered afterwards are automatically marked seen. - if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && - !knownMarker.RemoteSeenOn.Contains(partialAccountId)) - updateSeenMarkers = true; - - continue; - } - - currentFloorMarkers.Add(visibleMarker); - recreateLayout = true; - saveMarkers = true; + Task.Run(async () => + await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, locationsToSync)); } - if (!recreateLayout && currentFloorMarkers.Count > 0 && + 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 { - foreach (var marker in currentFloorMarkers) + foreach (var location in memoryTerritory.Locations) { - uint desiredColor = DetermineColor(marker, visibleMarkers); - if (marker.RenderElement == null || !marker.RenderElement.IsValid) - { - recreateLayout = true; - break; - } + uint desiredColor = DetermineColor(location, visibleLocations); + if (location.RenderElement == null || !location.RenderElement.IsValid) + return true; - if (marker.RenderElement.Color != desiredColor) - marker.RenderElement.Color = desiredColor; + if (location.RenderElement.Color != desiredColor) + location.RenderElement.Color = desiredColor; } } catch (Exception e) { _debugState.SetFromException(e); - recreateLayout = true; + return true; } } - if (updateSeenMarkers && partialAccountId != null) + return false; + } + + private void UploadLocations() + { + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory == null) + return; + + List locationsToUpload = memoryTerritory.Locations + .Where(loc => loc.NetworkId == null && loc.UploadRequested == false) + .ToList(); + if (locationsToUpload.Count > 0) { - var markersToUpdate = currentFloorMarkers.Where(x => - x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && - !x.RemoteSeenOn.Contains(partialAccountId)).ToList(); - foreach (var marker in markersToUpdate) - marker.RemoteSeenRequested = true; - Task.Run(async () => await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, markersToUpdate)); - } + foreach (var location in locationsToUpload) + location.UploadRequested = true; - if (saveMarkers) - { - currentFloor.Save(); - - if (_territoryState.TerritorySyncState == SyncState.Complete) - { - var markersToUpload = currentFloorMarkers - .Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); - if (markersToUpload.Count > 0) - { - foreach (var marker in markersToUpload) - marker.UploadRequested = true; - Task.Run(async () => - await UploadMarkersForTerritory(_territoryState.LastTerritory, markersToUpload)); - } - } - } - - if (recreateLayout) - { - _renderAdapter.ResetLayer(ELayer.TrapHoard); - - List elements = new(); - foreach (var marker in currentFloorMarkers) - { - if (marker.Seen || _configuration.Mode == EMode.Online || - marker is { WasImported: true, Imports.Count: > 0 }) - { - if (marker.Type == Marker.EType.Trap) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), - _configuration.DeepDungeons.Traps); - } - else if (marker.Type == Marker.EType.Hoard) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), - _configuration.DeepDungeons.HoardCoffers); - } - } - } - - if (elements.Count == 0) - return; - - _renderAdapter.SetLayer(ELayer.TrapHoard, elements); + Task.Run(async () => + await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload)); } } - private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) + private void RecreatePersistentLayout(IReadOnlyList visibleMarkers) { - recreateLayout |= - _floorService.EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); - recreateLayout |= - visibleMarkers.Any(visibleMarker => _floorService.EphemeralMarkers.All(x => x != visibleMarker)); + _renderAdapter.ResetLayer(ELayer.TrapHoard); - if (recreateLayout) + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory == null) + return; + + List elements = new(); + foreach (var location in memoryTerritory.Locations) { - _renderAdapter.ResetLayer(ELayer.RegularCoffers); - _floorService.EphemeralMarkers.Clear(); - - List elements = new(); - foreach (var marker in visibleMarkers) + if (location.Type == MemoryLocation.EType.Trap) { - _floorService.EphemeralMarkers.Add(marker); - - if (marker.Type == Marker.EType.SilverCoffer && _configuration.DeepDungeons.SilverCoffers.Show) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), - _configuration.DeepDungeons.SilverCoffers); - } + 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); } - - if (elements.Count == 0) - return; - - _renderAdapter.SetLayer(ELayer.RegularCoffers, elements); } + + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.TrapHoard, elements); } - private uint DetermineColor(Marker marker, IList visibleMarkers) + private void RecreateEphemeralLayout() { - switch (marker.Type) + _renderAdapter.ResetLayer(ELayer.RegularCoffers); + + List elements = new(); + foreach (var location in _floorService.EphemeralLocations) { - case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive || - !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || - visibleMarkers.Any(x => x == marker): + if (location.Type == MemoryLocation.EType.SilverCoffer && + _configuration.DeepDungeons.SilverCoffers.Show) + { + CreateRenderElement(location, elements, DetermineColor(location), + _configuration.DeepDungeons.SilverCoffers); + } + } + + 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 Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive || - !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || - visibleMarkers.Any(x => x == marker): + case MemoryLocation.EType.Hoard + when _territoryState.PomanderOfIntuition == PomanderState.Inactive || + !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || + visibleLocations.Any(x => x == location): return _configuration.DeepDungeons.HoardCoffers.Color; - case Marker.EType.SilverCoffer: - return _configuration.DeepDungeons.SilverCoffers.Color; - case Marker.EType.Trap: - case Marker.EType.Hoard: - return RenderData.ColorInvisible; default: - return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); + return RenderData.ColorInvisible; } } - private void CreateRenderElement(Marker marker, List elements, uint color, + private uint DetermineColor(EphemeralLocation location) + { + if (location.Type == MemoryLocation.EType.SilverCoffer) + return _configuration.DeepDungeons.SilverCoffers.Color; + + return RenderData.ColorInvisible; + } + + private void CreateRenderElement(MemoryLocation location, List elements, uint color, MarkerConfiguration config) { if (!config.Show) return; - var element = _renderAdapter.CreateElement(marker.Type, marker.Position, color, config.Fill); - marker.RenderElement = element; + var element = _renderAdapter.CreateElement(location.Type, location.Position, color, config.Fill); + location.RenderElement = element; elements.Add(element); } @@ -325,7 +306,7 @@ namespace Pal.Client.DependencyInjection Type = SyncType.Download, TerritoryType = territoryId, Success = success, - Markers = downloadedMarkers + Locations = downloadedMarkers }); } catch (Exception e) @@ -334,17 +315,17 @@ namespace Pal.Client.DependencyInjection } } - private async Task UploadMarkersForTerritory(ushort territoryId, List markersToUpload) + private async Task UploadLocationsForTerritory(ushort territoryId, List locationsToUpload) { try { - var (success, uploadedMarkers) = await _remoteApi.UploadMarker(territoryId, markersToUpload); + var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload); LateEventQueue.Enqueue(new QueuedSyncResponse { Type = SyncType.Upload, TerritoryType = territoryId, Success = success, - Markers = uploadedMarkers + Locations = uploadedLocations }); } catch (Exception e) @@ -353,17 +334,18 @@ namespace Pal.Client.DependencyInjection } } - private async Task SyncSeenMarkersForTerritory(ushort territoryId, List markersToUpdate) + private async Task SyncSeenMarkersForTerritory(ushort territoryId, + IReadOnlyList locationsToUpdate) { try { - var success = await _remoteApi.MarkAsSeen(territoryId, markersToUpdate); + var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate); LateEventQueue.Enqueue(new QueuedSyncResponse { Type = SyncType.MarkSeen, TerritoryType = territoryId, Success = success, - Markers = markersToUpdate, + Locations = locationsToUpdate, }); } catch (Exception e) @@ -374,9 +356,10 @@ namespace Pal.Client.DependencyInjection #endregion - private IList GetRelevantGameObjects() + private (IReadOnlyList, IReadOnlyList) GetRelevantGameObjects() { - List result = new(); + List persistentLocations = new(); + List ephemeralLocations = new(); for (int i = 246; i < _objectTable.Length; i++) { GameObject? obj = _objectTable[i]; @@ -391,16 +374,31 @@ namespace Pal.Client.DependencyInjection case 2007185: case 2007186: case 2009504: - result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); + persistentLocations.Add(new PersistentLocation + { + Type = MemoryLocation.EType.Trap, + Position = obj.Position, + Seen = true + }); break; case 2007542: case 2007543: - result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true }); + persistentLocations.Add(new PersistentLocation + { + Type = MemoryLocation.EType.Hoard, + Position = obj.Position, + Seen = true + }); break; case 2007357: - result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true }); + ephemeralLocations.Add(new EphemeralLocation + { + Type = MemoryLocation.EType.SilverCoffer, + Position = obj.Position, + Seen = true + }); break; } } @@ -409,18 +407,25 @@ namespace Pal.Client.DependencyInjection { var obj = _objectTable.FirstOrDefault(x => x.Address == address); if (obj != null && obj.Position.Length() > 0.1) - result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); + { + persistentLocations.Add(new PersistentLocation + { + Type = MemoryLocation.EType.Trap, + Position = obj.Position, + Seen = true, + }); + } } - return result; + return (persistentLocations, ephemeralLocations); } - private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) + 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, ref saveMarkers); + handler.RunIfCompatible(queued, ref recreateLayout); } } } diff --git a/Pal.Client/DependencyInjection/ImportService.cs b/Pal.Client/DependencyInjection/ImportService.cs index 1d5f3cb..4d074dd 100644 --- a/Pal.Client/DependencyInjection/ImportService.cs +++ b/Pal.Client/DependencyInjection/ImportService.cs @@ -3,21 +3,28 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Account; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Pal.Client.Database; +using Pal.Client.Floors; +using Pal.Client.Floors.Tasks; +using Pal.Common; namespace Pal.Client.DependencyInjection { internal sealed class ImportService { private readonly IServiceProvider _serviceProvider; + private readonly FloorService _floorService; - public ImportService(IServiceProvider serviceProvider) + public ImportService(IServiceProvider serviceProvider, FloorService floorService) { _serviceProvider = serviceProvider; + _floorService = floorService; } + /* public void Add(ImportHistory history) { using var scope = _serviceProvider.CreateScope(); @@ -26,6 +33,7 @@ namespace Pal.Client.DependencyInjection dbContext.Imports.Add(history); dbContext.SaveChanges(); } + */ public async Task FindLast(CancellationToken token = default) { @@ -35,6 +43,7 @@ namespace Pal.Client.DependencyInjection return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefaultAsync(cancellationToken: token); } + /* public List FindForServer(string server) { if (string.IsNullOrEmpty(server)) @@ -44,18 +53,58 @@ namespace Pal.Client.DependencyInjection using var dbContext = scope.ServiceProvider.GetRequiredService(); return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList(); - } + }*/ - public void RemoveAllByIds(List ids) + public (int traps, int hoard) Import(ExportRoot import) { using var scope = _serviceProvider.CreateScope(); using var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.RemoveRange(dbContext.Imports.Where(x => ids.Contains(x.Id))); + dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList()); + + ImportHistory importHistory = new ImportHistory + { + 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 newLocation in floor.Objects) + { + throw new NotImplementedException(); + } + } + // TODO filter here, update territories dbContext.SaveChanges(); + + _floorService.ResetAll(); + return (traps, hoard); } public void RemoveById(Guid id) - => RemoveAllByIds(new List { id }); + { + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id)); + + // TODO filter here, update territories + dbContext.SaveChanges(); + + _floorService.ResetAll(); + } } } diff --git a/Pal.Client/DependencyInjection/TerritoryState.cs b/Pal.Client/DependencyInjection/TerritoryState.cs index 15e21d4..75c3197 100644 --- a/Pal.Client/DependencyInjection/TerritoryState.cs +++ b/Pal.Client/DependencyInjection/TerritoryState.cs @@ -17,7 +17,7 @@ namespace Pal.Client.DependencyInjection } public ushort LastTerritory { get; set; } - public SyncState TerritorySyncState { get; set; } + public ESyncState TerritorySyncState { get; set; } public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 3ef0b58..ac3b722 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -24,6 +24,7 @@ using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection.Logging; using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Net; using Pal.Client.Properties; using Pal.Client.Rendering; @@ -63,7 +64,8 @@ namespace Pal.Client CommandManager commandManager, DataManager dataManager) { - _logger.LogInformation("Building service container"); + _logger.LogInformation("Building service container for {Assembly}", + typeof(DependencyInjectionContext).Assembly.FullName); // set up legacy services #pragma warning disable CS0612 diff --git a/Pal.Client/Floors/EphemeralLocation.cs b/Pal.Client/Floors/EphemeralLocation.cs new file mode 100644 index 0000000..ccf8a4b --- /dev/null +++ b/Pal.Client/Floors/EphemeralLocation.cs @@ -0,0 +1,24 @@ +using System; + +namespace Pal.Client.Floors +{ + /// + /// 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) + { + return Equals(a, b); + } + + public static bool operator !=(EphemeralLocation? a, object? b) + { + return !Equals(a, b); + } + } +} diff --git a/Pal.Client/Floors/FloorService.cs b/Pal.Client/Floors/FloorService.cs new file mode 100644 index 0000000..535b418 --- /dev/null +++ b/Pal.Client/Floors/FloorService.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Configuration; +using Pal.Client.Extensions; +using Pal.Client.Floors.Tasks; +using Pal.Client.Net; +using Pal.Common; + +namespace Pal.Client.Floors +{ + internal sealed class FloorService + { + private readonly IPalacePalConfiguration _configuration; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IReadOnlyDictionary _territories; + + private ConcurrentBag _ephemeralLocations = new(); + + public FloorService(IPalacePalConfiguration configuration, IServiceScopeFactory serviceScopeFactory) + { + _configuration = configuration; + _serviceScopeFactory = serviceScopeFactory; + _territories = Enum.GetValues().ToDictionary(o => o, o => new MemoryTerritory(o)); + } + + public IReadOnlyCollection EphemeralLocations => _ephemeralLocations; + + 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.IsReady && !territory.IsLoading) + { + territory.IsLoading = true; + new LoadTerritory(_serviceScopeFactory, 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.IsReady) + 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) + { + 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; + } + + territory.Locations.Add(visibleLocation); + newLocations.Add(visibleLocation); + recreateLayout = true; + } + + if (markAsSeen.Count > 0) + new MarkAsSeen(_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() + { + foreach (var memoryTerritory in _territories.Values) + { + lock (memoryTerritory.LockObj) + memoryTerritory.Reset(); + } + } + } +} diff --git a/Pal.Client/Floors/MemoryLocation.cs b/Pal.Client/Floors/MemoryLocation.cs new file mode 100644 index 0000000..296bfcd --- /dev/null +++ b/Pal.Client/Floors/MemoryLocation.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Pal.Client.Rendering; +using Pal.Common; +using Palace; + +namespace Pal.Client.Floors +{ + /// + /// 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 + { + Unknown, + + Hoard, + Trap, + + SilverCoffer, + } + + 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)); + } + } + + 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) + }; + } + } +} diff --git a/Pal.Client/Floors/MemoryTerritory.cs b/Pal.Client/Floors/MemoryTerritory.cs new file mode 100644 index 0000000..dc835ec --- /dev/null +++ b/Pal.Client/Floors/MemoryTerritory.cs @@ -0,0 +1,49 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Pal.Client.Configuration; +using Pal.Common; + +namespace Pal.Client.Floors +{ + /// + /// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60. + /// + internal sealed class MemoryTerritory + { + public MemoryTerritory(ETerritoryType territoryType) + { + TerritoryType = territoryType; + } + + public ETerritoryType TerritoryType { get; } + public bool IsReady { get; set; } + public bool IsLoading { get; set; } + + 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); + + IsReady = true; + IsLoading = false; + } + + public IEnumerable GetRemovableLocations(EMode mode) + { + // TODO there was better logic here; + return Locations.Where(x => !x.Seen); + } + + public void Reset() + { + Locations.Clear(); + IsReady = false; + IsLoading = false; + } + } +} diff --git a/Pal.Client/Floors/PersistentLocation.cs b/Pal.Client/Floors/PersistentLocation.cs new file mode 100644 index 0000000..d1db189 --- /dev/null +++ b/Pal.Client/Floors/PersistentLocation.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Pal.Client.Database; + +namespace Pal.Client.Floors +{ + /// + /// A loaded in memory, with certain extra attributes as needed. + /// + internal sealed class PersistentLocation : MemoryLocation + { + /// + public int? LocalId { get; set; } + + /// + /// Network id for the server you're currently connected to. + /// + 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 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); + } + } +} diff --git a/Pal.Client/Floors/Tasks/DbTask.cs b/Pal.Client/Floors/Tasks/DbTask.cs new file mode 100644 index 0000000..017f96d --- /dev/null +++ b/Pal.Client/Floors/Tasks/DbTask.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Database; + +namespace Pal.Client.Floors.Tasks +{ + internal abstract class DbTask + { + private readonly IServiceScopeFactory _serviceScopeFactory; + + protected DbTask(IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + } + + public void Start() + { + Task.Run(() => + { + using var scope = _serviceScopeFactory.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + Run(dbContext); + }); + } + + protected abstract void Run(PalClientContext dbContext); + } +} diff --git a/Pal.Client/Floors/Tasks/LoadTerritory.cs b/Pal.Client/Floors/Tasks/LoadTerritory.cs new file mode 100644 index 0000000..caa4291 --- /dev/null +++ b/Pal.Client/Floors/Tasks/LoadTerritory.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Database; + +namespace Pal.Client.Floors.Tasks +{ + internal sealed class LoadTerritory : DbTask + { + private readonly MemoryTerritory _territory; + + public LoadTerritory(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory) + : base(serviceScopeFactory) + { + _territory = territory; + } + + protected override void Run(PalClientContext dbContext) + { + lock (_territory.LockObj) + { + if (_territory.IsReady) + return; + + List locations = dbContext.Locations + .Where(o => o.TerritoryType == (ushort)_territory.TerritoryType) + .Include(o => o.ImportedBy) + .Include(o => o.RemoteEncounters) + .ToList(); + _territory.Initialize(locations.Select(ToMemoryLocation)); + } + } + + 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, + 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/MarkAsSeen.cs b/Pal.Client/Floors/Tasks/MarkAsSeen.cs new file mode 100644 index 0000000..3e1b767 --- /dev/null +++ b/Pal.Client/Floors/Tasks/MarkAsSeen.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Database; + +namespace Pal.Client.Floors.Tasks +{ + internal sealed class MarkAsSeen : DbTask + { + private readonly MemoryTerritory _territory; + private readonly IReadOnlyList _locations; + + public MarkAsSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, + IReadOnlyList locations) + : base(serviceScopeFactory) + { + _territory = territory; + _locations = locations; + } + + protected override void Run(PalClientContext dbContext) + { + lock (_territory.LockObj) + { + dbContext.Locations + .Where(loc => _locations.Any(l => l.LocalId == 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 new file mode 100644 index 0000000..16a20c8 --- /dev/null +++ b/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Database; + +namespace Pal.Client.Floors.Tasks +{ + 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) + { + _territory = territory; + _locations = locations; + _accountId = accountId; + } + + protected override void Run(PalClientContext dbContext) + { + lock (_territory.LockObj) + { + List locationsToUpdate = dbContext.Locations + .Where(loc => _locations.Any(l => + l.LocalId == loc.LocalId && loc.RemoteEncounters.All(r => r.AccountId != _accountId))) + .ToList(); + foreach (var clientLocation in locationsToUpdate) + 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 new file mode 100644 index 0000000..4489740 --- /dev/null +++ b/Pal.Client/Floors/Tasks/SaveNewLocations.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Database; +using Pal.Common; + +namespace Pal.Client.Floors.Tasks +{ + internal sealed class SaveNewLocations : DbTask + { + private readonly MemoryTerritory _territory; + private readonly List _newLocations; + + public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, + List newLocations) + : base(serviceScopeFactory) + { + _territory = territory; + _newLocations = newLocations; + } + + protected override void Run(PalClientContext dbContext) + { + Run(_territory, dbContext, _newLocations); + } + + public static void Run(MemoryTerritory territory, PalClientContext dbContext, + 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) + { + persistentLocation.LocalId = clientLocation.LocalId; + } + } + } + + private static ClientLocation ToDatabaseLocation(PersistentLocation location, ETerritoryType territoryType) + { + 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, + SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2), + }; + } + + private static ClientLocation.EType ToDatabaseType(MemoryLocation.EType type) + { + return type switch + { + MemoryLocation.EType.Trap => ClientLocation.EType.Trap, + MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } + } +} diff --git a/Pal.Client/LocalState.cs b/Pal.Client/LocalState.cs deleted file mode 100644 index 8534c50..0000000 --- a/Pal.Client/LocalState.cs +++ /dev/null @@ -1,159 +0,0 @@ -using Pal.Common; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using Pal.Client.Configuration; -using Pal.Client.Extensions; - -namespace Pal.Client -{ - /// - /// JSON for a single floor set (e.g. 51-60). - /// - internal sealed class LocalState - { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; - private const int CurrentVersion = 4; - - internal static string PluginConfigDirectory { get; set; } = null!; - internal static EMode Mode { get; set; } - - public uint TerritoryType { get; set; } - public ConcurrentBag Markers { get; set; } = new(); - - public LocalState(uint territoryType) - { - TerritoryType = territoryType; - } - - private void ApplyFilters() - { - if (Mode == EMode.Offline) - Markers = new ConcurrentBag(Markers.Where(x => x.Seen || (x.WasImported && x.Imports.Count > 0))); - else - // ensure old import markers are removed if they are no longer part of a "current" import - // this MAY remove markers the server sent you (and that you haven't seen), but this should be fixed the next time you enter the zone - Markers = new ConcurrentBag(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0)); - } - - public static LocalState? Load(uint territoryType) - { - string path = GetSaveLocation(territoryType); - if (!File.Exists(path)) - return null; - - string content = File.ReadAllText(path); - if (content.Length == 0) - return null; - - LocalState localState; - int version = 1; - if (content[0] == '[') - { - // v1 only had a list of markers, not a JSON object as root - localState = new LocalState(territoryType) - { - Markers = new ConcurrentBag(JsonSerializer.Deserialize>(content, JsonSerializerOptions) ?? new()), - }; - } - else - { - var save = JsonSerializer.Deserialize(content, JsonSerializerOptions); - if (save == null) - return null; - - localState = new LocalState(territoryType) - { - Markers = new ConcurrentBag(save.Markers.Where(o => o.Type == Marker.EType.Trap || o.Type == Marker.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; - } - - 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); - } - } - - 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()) - { - LocalState? 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/Marker.cs b/Pal.Client/Marker.cs deleted file mode 100644 index e2e79b9..0000000 --- a/Pal.Client/Marker.cs +++ /dev/null @@ -1,110 +0,0 @@ -using ECommons.SplatoonAPI; -using Pal.Client.Rendering; -using Pal.Common; -using Palace; -using System; -using System.Collections.Generic; -using System.Numerics; -using System.Text.Json.Serialization; - -namespace Pal.Client -{ - internal sealed class Marker - { - public EType Type { get; set; } = EType.Unknown; - public Vector3 Position { get; set; } - - /// - /// Whether we have encountered the trap/coffer at this location in-game. - /// - public bool Seen { get; set; } - - /// - /// Network id for the server you're currently connected to. - /// - [JsonIgnore] - public Guid? NetworkId { get; set; } - - /// - /// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests. - /// - [JsonIgnore] - public bool UploadRequested { get; set; } - - /// - /// Which account ids this marker was seen. This is a list merely to support different remote endpoints - /// (where each server would assign you a different id). - /// - public List RemoteSeenOn { get; set; } = new(); - - /// - /// Whether this marker was requested to be seen, to avoid duplicate requests. - /// - [JsonIgnore] - public bool RemoteSeenRequested { get; set; } - - /// - /// 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 Imports { get; set; } = new(); - - public bool WasImported { get; set; } - - /// - /// To make rollbacks of local data easier, keep track of the version which was used to write the marker initially. - /// - public string? SinceVersion { get; set; } - - [JsonIgnore] - public IRenderElement? RenderElement { get; set; } - - public Marker(EType type, Vector3 position, Guid? networkId = null) - { - Type = type; - Position = position; - NetworkId = networkId; - } - - public override int GetHashCode() - { - return HashCode.Combine(Type, PalaceMath.GetHashCode(Position)); - } - - public override bool Equals(object? obj) - { - return obj is Marker otherMarker && Type == otherMarker.Type && PalaceMath.IsNearlySamePosition(Position, otherMarker.Position); - } - - public static bool operator ==(Marker? a, object? b) - { - return Equals(a, b); - } - - public static bool operator !=(Marker? a, object? b) - { - return !Equals(a, b); - } - - - public bool IsPermanent() => Type == EType.Trap || Type == EType.Hoard; - - public enum EType - { - Unknown = ObjectType.Unknown, - - #region Permanent Markers - Trap = ObjectType.Trap, - Hoard = ObjectType.Hoard, - - [Obsolete] - Debug = 3, - #endregion - - # region Markers that only show up if they're currently visible - SilverCoffer = 100, - #endregion - } - } -} diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs index 259b1ea..cb4dbde 100644 --- a/Pal.Client/Net/RemoteApi.PalaceService.cs +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -5,24 +5,25 @@ using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; +using Pal.Client.Floors; namespace Pal.Client.Net { 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(CreateMarkerFromNetworkObject).ToList()); + return (downloadReply.Success, downloadReply.Objects.Select(CreateLocationFromNetworkObject).ToList()); } - public async Task<(bool, List)> UploadMarker(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + public async Task<(bool, List)> UploadLocations(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) { - if (markers.Count == 0) + if (locations.Count == 0) return (true, new()); if (!await Connect(cancellationToken)) @@ -33,7 +34,7 @@ namespace Pal.Client.Net { TerritoryType = territoryType, }; - uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject + uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject { Type = (ObjectType)m.Type, X = m.Position.X, @@ -41,12 +42,12 @@ namespace Pal.Client.Net Z = m.Position.Z })); var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); - return (uploadReply.Success, uploadReply.Objects.Select(CreateMarkerFromNetworkObject).ToList()); + return (uploadReply.Success, uploadReply.Objects.Select(CreateLocationFromNetworkObject).ToList()); } - public async Task MarkAsSeen(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + public async Task MarkAsSeen(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) { - if (markers.Count == 0) + if (locations.Count == 0) return true; if (!await Connect(cancellationToken)) @@ -54,15 +55,22 @@ namespace Pal.Client.Net var palaceClient = new PalaceService.PalaceServiceClient(_channel); var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; - foreach (var marker in markers) + 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 Marker CreateMarkerFromNetworkObject(PalaceObject obj) => - new Marker((Marker.EType)obj.Type, new Vector3(obj.X, obj.Y, obj.Z), Guid.Parse(obj.NetworkId)); + 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), + }; + } public async Task<(bool, List)> FetchStatistics(CancellationToken cancellationToken = default) { diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 83b35a3..6815210 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -13,7 +13,7 @@ namespace Pal.Client.Net #if DEBUG public const string RemoteUrl = "http://localhost:5145"; #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)}"; diff --git a/Pal.Client/Rendering/IRenderer.cs b/Pal.Client/Rendering/IRenderer.cs index 0ac7fd3..1856403 100644 --- a/Pal.Client/Rendering/IRenderer.cs +++ b/Pal.Client/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Numerics; using Pal.Client.Configuration; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -12,7 +13,7 @@ namespace Pal.Client.Rendering void ResetLayer(ELayer layer); - IRenderElement CreateElement(Marker.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); } diff --git a/Pal.Client/Rendering/MarkerConfig.cs b/Pal.Client/Rendering/MarkerConfig.cs index 2ef9dde..0bc25cf 100644 --- a/Pal.Client/Rendering/MarkerConfig.cs +++ b/Pal.Client/Rendering/MarkerConfig.cs @@ -1,20 +1,23 @@ using System.Collections.Generic; +using Pal.Client.Floors; namespace Pal.Client.Rendering { internal sealed class MarkerConfig { private static readonly MarkerConfig EmptyConfig = new(); - private static readonly Dictionary MarkerConfigs = new() + + private static readonly Dictionary MarkerConfigs = new() { - { Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } }, - { Marker.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } }, - { Marker.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, + { 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 } }, }; public float OffsetY { get; private init; } public float Radius { get; private init; } = 0.25f; - public static MarkerConfig ForType(Marker.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 5ccb1a1..dfc2287 100644 --- a/Pal.Client/Rendering/RenderAdapter.cs +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -4,6 +4,7 @@ using System.Numerics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -56,7 +57,7 @@ namespace Pal.Client.Rendering public void ResetLayer(ELayer layer) => _implementation.ResetLayer(layer); - public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) + public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) => _implementation.CreateElement(type, pos, color, fill); public ERenderer GetConfigValue() diff --git a/Pal.Client/Rendering/SimpleRenderer.cs b/Pal.Client/Rendering/SimpleRenderer.cs index d7724f5..3143def 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -9,6 +9,7 @@ using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -53,7 +54,7 @@ namespace Pal.Client.Rendering l.Dispose(); } - public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) + public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) { var config = MarkerConfig.ForType(type); return new SimpleElement @@ -73,9 +74,13 @@ namespace Pal.Client.Rendering TerritoryType = _clientState.TerritoryType, Elements = new List { - (SimpleElement)CreateElement(Marker.EType.Trap, _clientState.LocalPlayer?.Position ?? default, + (SimpleElement)CreateElement( + MemoryLocation.EType.Trap, + _clientState.LocalPlayer?.Position ?? default, trapColor), - (SimpleElement)CreateElement(Marker.EType.Hoard, _clientState.LocalPlayer?.Position ?? default, + (SimpleElement)CreateElement( + MemoryLocation.EType.Hoard, + _clientState.LocalPlayer?.Position ?? default, hoardColor) }, ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout @@ -120,15 +125,15 @@ namespace Pal.Client.Rendering switch (e.Type) { - case Marker.EType.Hoard: + 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 Marker.EType.Trap; + goto case MemoryLocation.EType.Trap; - case Marker.EType.Trap: + case MemoryLocation.EType.Trap: var playerPos = _clientState.LocalPlayer?.Position; if (playerPos == null) return; @@ -189,7 +194,7 @@ namespace Pal.Client.Rendering public sealed class SimpleElement : IRenderElement { public bool IsValid { get; set; } = true; - public required Marker.EType Type { get; init; } + 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; } diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 82dd7f2..bdcee3d 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -13,6 +13,7 @@ using Dalamud.Game.ClientState; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -57,7 +58,8 @@ namespace Pal.Client.Rendering } catch (Exception e) { - _logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer, elements.Count); + _logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer, + elements.Count); _debugState.SetFromException(e); } }); @@ -78,7 +80,7 @@ namespace Pal.Client.Rendering private string ToLayerName(ELayer layer) => $"PalacePal.{layer}"; - public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) + public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) { MarkerConfig config = MarkerConfig.ForType(type); Element element = new Element(ElementType.CircleAtFixedCoordinates) @@ -109,8 +111,8 @@ namespace Pal.Client.Rendering var elements = new List { - CreateElement(Marker.EType.Trap, pos.Value, trapColor), - CreateElement(Marker.EType.Hoard, pos.Value, hoardColor), + CreateElement(MemoryLocation.EType.Trap, pos.Value, trapColor), + CreateElement(MemoryLocation.EType.Hoard, pos.Value, hoardColor), }; if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index 2796971..c86ad36 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -8,7 +8,7 @@ namespace Pal.Client.Scheduled { internal interface IHandler { - void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers); + void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout); } internal abstract class Handler : IHandler @@ -21,14 +21,14 @@ namespace Pal.Client.Scheduled _logger = logger; } - protected abstract void Run(T queued, ref bool recreateLayout, ref bool saveMarkers); + protected abstract void Run(T queued, ref bool recreateLayout); - public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) + public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout) { if (queued is T t) { _logger.LogInformation("Handling {QueuedType}", queued.GetType()); - Run(t, ref recreateLayout, ref saveMarkers); + Run(t, ref recreateLayout); } else { diff --git a/Pal.Client/Scheduled/QueuedConfigUpdate.cs b/Pal.Client/Scheduled/QueuedConfigUpdate.cs index d0e71ca..9810f94 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; using Pal.Client.Rendering; namespace Pal.Client.Scheduled @@ -9,38 +10,19 @@ namespace Pal.Client.Scheduled { internal sealed class Handler : IQueueOnFrameworkThread.Handler { - private readonly IPalacePalConfiguration _configuration; - private readonly FloorService _floorService; - private readonly TerritoryState _territoryState; private readonly RenderAdapter _renderAdapter; public Handler( ILogger logger, - IPalacePalConfiguration configuration, - FloorService floorService, - TerritoryState territoryState, RenderAdapter renderAdapter) : base(logger) { - _configuration = configuration; - _floorService = floorService; - _territoryState = territoryState; _renderAdapter = renderAdapter; } - protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout, ref bool saveMarkers) + protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout) { - if (_configuration.Mode == EMode.Offline) - { - LocalState.UpdateAll(); - _floorService.FloorMarkers.Clear(); - _floorService.EphemeralMarkers.Clear(); - _territoryState.LastTerritory = 0; - - recreateLayout = true; - saveMarkers = true; - } - + // TODO filter stuff if offline _renderAdapter.ConfigUpdated(); } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 2e35c71..89a947b 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -1,16 +1,12 @@ using Account; using Pal.Common; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Game.Gui; -using Dalamud.Logging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; using Pal.Client.DependencyInjection; -using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Properties; using Pal.Client.Windows; @@ -31,63 +27,46 @@ namespace Pal.Client.Scheduled internal sealed class Handler : IQueueOnFrameworkThread.Handler { + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly Chat _chat; - private readonly FloorService _floorService; private readonly ImportService _importService; private readonly ConfigWindow _configWindow; public Handler( ILogger logger, + IServiceScopeFactory serviceScopeFactory, Chat chat, - FloorService floorService, ImportService importService, ConfigWindow configWindow) : base(logger) { + _serviceScopeFactory = serviceScopeFactory; _chat = chat; - _floorService = floorService; _importService = importService; _configWindow = configWindow; } - protected override void Run(QueuedImport import, ref bool recreateLayout, ref bool saveMarkers) + protected override void Run(QueuedImport import, ref bool recreateLayout) { recreateLayout = true; - saveMarkers = true; try { if (!Validate(import)) return; - List oldExportIds = _importService.FindForServer(import.Export.ServerUrl) - .Select(x => x.Id) - .ToList(); - foreach (var remoteFloor in import.Export.Floors) + using (var scope = _serviceScopeFactory.CreateScope()) { - ushort territoryType = (ushort)remoteFloor.TerritoryType; - var localState = _floorService.GetFloorMarkers(territoryType); - - localState.UndoImport(oldExportIds); - ImportFloor(import, remoteFloor, localState); - - localState.Save(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + (import.ImportedTraps, import.ImportedHoardCoffers) = _importService.Import(import.Export); } - _importService.RemoveAllByIds(oldExportIds); - _importService.RemoveById(import.ExportId); - _importService.Add(new ImportHistory - { - Id = import.ExportId, - RemoteUrl = import.Export.ServerUrl, - ExportedAt = import.Export.CreatedAt.ToDateTime(), - ImportedAt = DateTime.UtcNow, - }); _configWindow.UpdateLastImport(); _logger.LogInformation( - $"Imported {import.ExportId} for {import.ImportedTraps} traps, {import.ImportedHoardCoffers} hoard coffers"); + "Imported {ExportId} for {Traps} traps, {Hoard} hoard coffers", import.ExportId, + import.ImportedTraps, import.ImportedHoardCoffers); _chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, import.ImportedHoardCoffers)); } @@ -103,7 +82,8 @@ namespace Pal.Client.Scheduled if (import.Export.ExportVersion != ExportConfig.ExportVersion) { _logger.LogError( - "Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}", import.Export.ExportVersion, ExportConfig.ExportVersion); + "Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}", + import.Export.ExportVersion, ExportConfig.ExportVersion); _chat.Error(Localization.Error_ImportFailed_IncompatibleVersion); return false; } @@ -127,28 +107,6 @@ namespace Pal.Client.Scheduled return true; } - - private void ImportFloor(QueuedImport import, ExportFloor remoteFloor, LocalState localState) - { - var remoteMarkers = remoteFloor.Objects.Select(m => - new Marker((Marker.EType)m.Type, new Vector3(m.X, m.Y, m.Z)) { WasImported = true }); - foreach (var remoteMarker in remoteMarkers) - { - Marker? localMarker = localState.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker == null) - { - localState.Markers.Add(remoteMarker); - localMarker = remoteMarker; - - if (localMarker.Type == Marker.EType.Trap) - import.ImportedTraps++; - else if (localMarker.Type == Marker.EType.Hoard) - import.ImportedHoardCoffers++; - } - - remoteMarker.Imports.Add(import.ExportId); - } - } } } } diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index edf72ef..beed1aa 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; +using Pal.Client.Floors; +using Pal.Client.Floors.Tasks; using Pal.Client.Net; namespace Pal.Client.Scheduled @@ -14,10 +17,11 @@ namespace Pal.Client.Scheduled public required SyncType Type { get; init; } public required ushort TerritoryType { get; init; } public required bool Success { get; init; } - public required List Markers { get; init; } + public required IReadOnlyList Locations { get; init; } internal sealed class Handler : IQueueOnFrameworkThread.Handler { + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IPalacePalConfiguration _configuration; private readonly FloorService _floorService; private readonly TerritoryState _territoryState; @@ -25,47 +29,57 @@ namespace Pal.Client.Scheduled public Handler( ILogger logger, + IServiceScopeFactory serviceScopeFactory, IPalacePalConfiguration configuration, FloorService floorService, TerritoryState territoryState, DebugState debugState) : base(logger) { + _serviceScopeFactory = serviceScopeFactory; _configuration = configuration; _floorService = floorService; _territoryState = territoryState; _debugState = debugState; } - protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout, ref bool saveMarkers) + protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout) { recreateLayout = true; - saveMarkers = true; try { - var remoteMarkers = queued.Markers; - var currentFloor = _floorService.GetFloorMarkers(queued.TerritoryType); - if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) + var remoteMarkers = queued.Locations; + var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType); + if (memoryTerritory != null && _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. - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) + PersistentLocation? localLocation = + memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker); + if (localLocation != null) { - localMarker.NetworkId = remoteMarker.NetworkId; + localLocation.NetworkId = remoteMarker.NetworkId; continue; } if (queued.Type == SyncType.Download) - currentFloor.Markers.Add(remoteMarker); + { + memoryTerritory.Locations.Add(remoteMarker); + newLocations.Add(remoteMarker); + } } + if (newLocations.Count > 0) + new SaveNewLocations(_serviceScopeFactory, memoryTerritory, newLocations).Start(); + break; case SyncType.MarkSeen: @@ -73,11 +87,23 @@ namespace Pal.Client.Scheduled _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); if (partialAccountId == null) break; + + List locationsToUpdate = new(); foreach (var remoteMarker in remoteMarkers) { - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - localMarker.RemoteSeenOn.Add(partialAccountId); + 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; @@ -91,22 +117,22 @@ namespace Pal.Client.Scheduled if (queued.Type == SyncType.Download) { if (queued.Success) - _territoryState.TerritorySyncState = SyncState.Complete; + _territoryState.TerritorySyncState = ESyncState.Complete; else - _territoryState.TerritorySyncState = SyncState.Failed; + _territoryState.TerritorySyncState = ESyncState.Failed; } } catch (Exception e) { _debugState.SetFromException(e); if (queued.Type == SyncType.Download) - _territoryState.TerritorySyncState = SyncState.Failed; + _territoryState.TerritorySyncState = ESyncState.Failed; } } } } - public enum SyncState + public enum ESyncState { NotAttempted, NotNeeded, diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index 9e0eb2c..f5b0d3c 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; using Pal.Client.Windows; using Pal.Common; @@ -20,28 +21,18 @@ namespace Pal.Client.Scheduled internal sealed class Handler : IQueueOnFrameworkThread.Handler { private readonly ImportService _importService; - private readonly FloorService _floorService; private readonly ConfigWindow _configWindow; - public Handler(ILogger logger, ImportService importService, FloorService floorService, ConfigWindow configWindow) + public Handler(ILogger logger, ImportService importService, ConfigWindow configWindow) : base(logger) { _importService = importService; - _floorService = floorService; _configWindow = configWindow; } - protected override void Run(QueuedUndoImport queued, ref bool recreateLayout, ref bool saveMarkers) + protected override void Run(QueuedUndoImport queued, ref bool recreateLayout) { recreateLayout = true; - saveMarkers = true; - - foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) - { - var localState = _floorService.GetFloorMarkers((ushort)territoryType); - localState.UndoImport(new List { queued.ExportId }); - localState.Save(); - } _importService.RemoveById(queued.ExportId); _configWindow.UpdateLastImport(); diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index fed5212..5c7c44e 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -25,6 +25,7 @@ using Pal.Client.Configuration; using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; +using Pal.Client.Floors; namespace Pal.Client.Windows { @@ -382,24 +383,25 @@ namespace Pal.Client.Windows ImGui.Text($"{_debugState.DebugMessage}"); ImGui.Indent(); - if (_floorService.FloorMarkers.TryGetValue(_territoryState.LastTerritory, out var currentFloor)) + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory != null) { if (_trapConfig.Show) { - int traps = currentFloor.Markers.Count(x => x.Type == Marker.EType.Trap); + int traps = memoryTerritory.Locations.Count(x => x.Type == MemoryLocation.EType.Trap); ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); } if (_hoardConfig.Show) { - int hoardCoffers = currentFloor.Markers.Count(x => x.Type == Marker.EType.Hoard); + 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.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); + _floorService.EphemeralLocations.Count(x => x.Type == MemoryLocation.EType.SilverCoffer); ImGui.Text( $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); } From e0d4a5d6766dd40218508a7e669c1837865c2616 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 21 Feb 2023 16:38:27 +0100 Subject: [PATCH 29/38] Namespace cleanup --- Pal.Client/Windows/ConfigWindow.cs | 5 ----- Pal.Client/Windows/StatisticsWindow.cs | 3 --- 2 files changed, 8 deletions(-) diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 9c81c23..77b6e13 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -3,7 +3,6 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using ECommons; using Google.Protobuf; using ImGuiNET; @@ -14,18 +13,14 @@ using System; using System.IO; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Dalamud.Game.Gui; using Microsoft.Extensions.Logging; using Pal.Client.Extensions; using Pal.Client.Properties; using Pal.Client.Configuration; using Pal.Client.Database; using Pal.Client.DependencyInjection; -using Pal.Client.Extensions; using Pal.Client.Floors; namespace Pal.Client.Windows diff --git a/Pal.Client/Windows/StatisticsWindow.cs b/Pal.Client/Windows/StatisticsWindow.cs index 90dfe5b..05da234 100644 --- a/Pal.Client/Windows/StatisticsWindow.cs +++ b/Pal.Client/Windows/StatisticsWindow.cs @@ -7,9 +7,6 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Pal.Client.Properties; -using System.ComponentModel.DataAnnotations; -using System.Runtime.CompilerServices; -using System.Reflection; namespace Pal.Client.Windows { From 802e0c4cde71c7485c9e034b2a3900c97e24f663 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 21 Feb 2023 17:32:13 +0100 Subject: [PATCH 30/38] Db: Backups --- Pal.Client/Configuration/ConfigurationV7.cs | 1 + .../Configuration/IPalacePalConfiguration.cs | 7 ++ Pal.Client/DependencyInjectionContext.cs | 8 +- Pal.Client/DependencyInjectionLoader.cs | 109 ++++++++++++++++-- 4 files changed, 109 insertions(+), 16 deletions(-) diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs index 236fea9..f25f2b5 100644 --- a/Pal.Client/Configuration/ConfigurationV7.cs +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -15,6 +15,7 @@ namespace Pal.Client.Configuration 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) { diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index c8a1ee8..848dc36 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -22,6 +22,7 @@ namespace Pal.Client.Configuration DeepDungeonConfiguration DeepDungeons { get; set; } RendererConfiguration Renderer { get; set; } + BackupConfiguration Backups { get; set; } IAccountConfiguration CreateAccount(string server, Guid accountId); IAccountConfiguration? FindAccount(string server); @@ -92,4 +93,10 @@ namespace Pal.Client.Configuration bool EncryptIfNeeded(); } + + public class BackupConfiguration + { + public int MinimumBackupsToKeep { get; set; } = 3; + public int DaysToDeleteAfter { get; set; } = 21; + } } diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index ac3b722..33d9724 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -1,8 +1,6 @@ -using System; -using System.Globalization; +using System.Globalization; using System.IO; using System.Threading; -using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; @@ -11,7 +9,6 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Plugin; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -23,7 +20,6 @@ using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection.Logging; -using Pal.Client.Extensions; using Pal.Client.Floors; using Pal.Client.Net; using Pal.Client.Properties; @@ -129,7 +125,7 @@ namespace Pal.Client services.AddScoped(); services.AddScoped(); - // these should maybe be scoped + // rendering services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Pal.Client/DependencyInjectionLoader.cs b/Pal.Client/DependencyInjectionLoader.cs index fce371c..5a0eec1 100644 --- a/Pal.Client/DependencyInjectionLoader.cs +++ b/Pal.Client/DependencyInjectionLoader.cs @@ -1,7 +1,13 @@ using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Dalamud.Plugin; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -44,16 +50,11 @@ namespace Pal.Client _logger.LogInformation("Starting async init"); chat = _serviceProvider.GetService(); - // initialize database - await using (var scope = _serviceProvider.CreateAsyncScope()) - { - _logger.LogInformation("Loading database & running migrations"); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); + await RemoveOldBackups(); + await CreateBackups(); + cancellationToken.ThrowIfCancellationRequested(); - // takes 2-3 seconds with initializing connections, loading driver etc. - await dbContext.Database.MigrateAsync(cancellationToken); - _logger.LogInformation("Completed database migrations"); - } + await RunMigrations(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); @@ -91,12 +92,100 @@ namespace Pal.Client catch (Exception e) { _logger.LogError(e, "Async load failed"); - InitCompleted?.Invoke(() => chat?.Error(string.Format(Localization.Error_LoadFailed, $"{e.GetType()} - {e.Message}"))); + InitCompleted?.Invoke(() => + chat?.Error(string.Format(Localization.Error_LoadFailed, $"{e.GetType()} - {e.Message}"))); LoadState = ELoadState.Error; } } + 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 match = backupRegex.Match(Path.GetFileName(path)); + if (!match.Success) + continue; + + if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal, out DateTime backupDate)) + { + backupFiles.Add((backupDate, path)); + } + } + + var toDelete = backupFiles.OrderByDescending(x => x.Date) + .Skip(configuration.Backups.MinimumBackupsToKeep) + .Where(x => (DateTime.Today.ToUniversalTime() - x.Date).Days > configuration.Backups.DaysToDeleteAfter) + .Select(x => x.Path); + foreach (var path in toDelete) + { + 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); + } + } + } + + private async Task CreateBackups() + { + await using var scope = _serviceProvider.CreateAsyncScope(); + + var pluginInterface = scope.ServiceProvider.GetRequiredService(); + string backupPath = Path.Join(pluginInterface.GetPluginConfigDirectory(), + $"backup-{DateTime.Today.ToUniversalTime():yyyy-MM-dd}.data.sqlite3"); + if (!File.Exists(backupPath)) + { + _logger.LogInformation("Creating database backup '{Path}'", 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("Database backup in '{Path}' already exists", backupPath); + } + + private async Task RunMigrations(CancellationToken cancellationToken) + { + // initialize database + 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"); + } + } + public enum ELoadState { Initializing, From d5dc55a0c4f4bc53b50e2ea36984da1580f89958 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 22 Feb 2023 17:21:48 +0100 Subject: [PATCH 31/38] Db: Fix various things around local persistence/net interactions --- .../DependencyInjection/FrameworkService.cs | 28 ++- .../DependencyInjection/TerritoryState.cs | 1 - Pal.Client/Floors/EphemeralLocation.cs | 5 + Pal.Client/Floors/FloorService.cs | 2 +- Pal.Client/Floors/MemoryLocation.cs | 12 +- Pal.Client/Floors/MemoryTerritory.cs | 4 +- Pal.Client/Floors/PersistentLocation.cs | 5 + Pal.Client/Floors/Tasks/DbTask.cs | 12 +- Pal.Client/Floors/Tasks/LoadTerritory.cs | 13 +- .../Tasks/{MarkAsSeen.cs => MarkLocalSeen.cs} | 9 +- Pal.Client/Floors/Tasks/MarkRemoteSeen.cs | 24 ++- Pal.Client/Floors/Tasks/SaveNewLocations.cs | 15 +- Pal.Client/Net/RemoteApi.AccountService.cs | 196 ++++++++++-------- Pal.Client/Net/RemoteApi.PalaceService.cs | 2 +- Pal.Client/Net/RemoteApi.cs | 2 +- Pal.Client/Plugin.cs | 2 +- Pal.Client/Scheduled/QueuedSyncResponse.cs | 23 +- Pal.Client/Windows/ConfigWindow.cs | 4 +- 18 files changed, 235 insertions(+), 124 deletions(-) rename Pal.Client/Floors/Tasks/{MarkAsSeen.cs => MarkLocalSeen.cs} (64%) diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs index c501410..0a8880e 100644 --- a/Pal.Client/DependencyInjection/FrameworkService.cs +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using ImGuiNET; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.Extensions; using Pal.Client.Floors; @@ -25,6 +26,7 @@ namespace Pal.Client.DependencyInjection internal sealed class FrameworkService : IDisposable { private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; private readonly Framework _framework; private readonly ConfigurationManager _configurationManager; private readonly IPalacePalConfiguration _configuration; @@ -42,6 +44,7 @@ namespace Pal.Client.DependencyInjection public FrameworkService( IServiceProvider serviceProvider, + ILogger logger, Framework framework, ConfigurationManager configurationManager, IPalacePalConfiguration configuration, @@ -54,6 +57,7 @@ namespace Pal.Client.DependencyInjection RemoteApi remoteApi) { _serviceProvider = serviceProvider; + _logger = logger; _framework = framework; _configurationManager = configurationManager; _configuration = configuration; @@ -92,8 +96,11 @@ namespace Pal.Client.DependencyInjection if (_territoryState.LastTerritory != _clientState.TerritoryType) { + MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (oldTerritory != null) + oldTerritory.SyncState = ESyncState.NotAttempted; + _territoryState.LastTerritory = _clientState.TerritoryType; - _territoryState.TerritorySyncState = ESyncState.NotAttempted; NextUpdateObjects.Clear(); _floorService.ChangeTerritory(_territoryState.LastTerritory); @@ -106,11 +113,12 @@ namespace Pal.Client.DependencyInjection if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory)) return; - if (_configuration.Mode == EMode.Online && - _territoryState.TerritorySyncState == ESyncState.NotAttempted) + ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory; + MemoryTerritory memoryTerritory = _floorService.GetTerritoryIfReady(territoryType)!; + if (_configuration.Mode == EMode.Online && memoryTerritory.SyncState == ESyncState.NotAttempted) { - _territoryState.TerritorySyncState = ESyncState.Started; - Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory)); + memoryTerritory.SyncState = ESyncState.Started; + Task.Run(async () => await DownloadLocationsForTerritory(_territoryState.LastTerritory)); } while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) @@ -120,7 +128,6 @@ namespace Pal.Client.DependencyInjection IReadOnlyList visibleEphemeralMarkers) = GetRelevantGameObjects(); - ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory; HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout); if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout)) @@ -188,7 +195,7 @@ namespace Pal.Client.DependencyInjection private void UploadLocations() { MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); - if (memoryTerritory == null) + if (memoryTerritory == null || memoryTerritory.SyncState != ESyncState.Complete) return; List locationsToUpload = memoryTerritory.Locations @@ -296,10 +303,11 @@ namespace Pal.Client.DependencyInjection #region Up-/Download - private async Task DownloadMarkersForTerritory(ushort territoryId) + private async Task DownloadLocationsForTerritory(ushort territoryId) { try { + _logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId); var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId); LateEventQueue.Enqueue(new QueuedSyncResponse { @@ -319,6 +327,8 @@ namespace Pal.Client.DependencyInjection { try { + _logger.LogInformation("Uploading {Count} locations for territory {Territory} to server", + locationsToUpload.Count, (ETerritoryType)territoryId); var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload); LateEventQueue.Enqueue(new QueuedSyncResponse { @@ -339,6 +349,8 @@ namespace Pal.Client.DependencyInjection { try { + _logger.LogInformation("Syncing {Count} seen locations for territory {Territory} to server", + locationsToUpdate.Count, (ETerritoryType)territoryId); var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate); LateEventQueue.Enqueue(new QueuedSyncResponse { diff --git a/Pal.Client/DependencyInjection/TerritoryState.cs b/Pal.Client/DependencyInjection/TerritoryState.cs index 75c3197..43852b8 100644 --- a/Pal.Client/DependencyInjection/TerritoryState.cs +++ b/Pal.Client/DependencyInjection/TerritoryState.cs @@ -17,7 +17,6 @@ namespace Pal.Client.DependencyInjection } public ushort LastTerritory { get; set; } - public ESyncState TerritorySyncState { get; set; } public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; diff --git a/Pal.Client/Floors/EphemeralLocation.cs b/Pal.Client/Floors/EphemeralLocation.cs index ccf8a4b..c4d8f20 100644 --- a/Pal.Client/Floors/EphemeralLocation.cs +++ b/Pal.Client/Floors/EphemeralLocation.cs @@ -20,5 +20,10 @@ namespace Pal.Client.Floors { return !Equals(a, b); } + + 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 535b418..8b53d75 100644 --- a/Pal.Client/Floors/FloorService.cs +++ b/Pal.Client/Floors/FloorService.cs @@ -111,7 +111,7 @@ namespace Pal.Client.Floors } if (markAsSeen.Count > 0) - new MarkAsSeen(_serviceScopeFactory, territory, markAsSeen).Start(); + new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start(); if (newLocations.Count > 0) new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start(); diff --git a/Pal.Client/Floors/MemoryLocation.cs b/Pal.Client/Floors/MemoryLocation.cs index 296bfcd..5b9a6ca 100644 --- a/Pal.Client/Floors/MemoryLocation.cs +++ b/Pal.Client/Floors/MemoryLocation.cs @@ -22,8 +22,8 @@ namespace Pal.Client.Floors { Unknown, - Hoard, Trap, + Hoard, SilverCoffer, } @@ -52,5 +52,15 @@ namespace Pal.Client.Floors _ => throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null) }; } + + public static ObjectType ToObjectType(this MemoryLocation.EType type) + { + return type switch + { + MemoryLocation.EType.Trap => ObjectType.Trap, + MemoryLocation.EType.Hoard => ObjectType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } } } diff --git a/Pal.Client/Floors/MemoryTerritory.cs b/Pal.Client/Floors/MemoryTerritory.cs index dc835ec..d0708ae 100644 --- a/Pal.Client/Floors/MemoryTerritory.cs +++ b/Pal.Client/Floors/MemoryTerritory.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Pal.Client.Configuration; +using Pal.Client.Scheduled; using Pal.Common; namespace Pal.Client.Floors @@ -18,7 +19,8 @@ namespace Pal.Client.Floors public ETerritoryType TerritoryType { get; } public bool IsReady { get; set; } - public bool IsLoading { get; set; } + public bool IsLoading { get; set; } // probably merge this with IsReady as enum + public ESyncState SyncState { get; set; } = ESyncState.NotAttempted; public ConcurrentBag Locations { get; } = new(); public object LockObj { get; } = new(); diff --git a/Pal.Client/Floors/PersistentLocation.cs b/Pal.Client/Floors/PersistentLocation.cs index d1db189..99cc5dd 100644 --- a/Pal.Client/Floors/PersistentLocation.cs +++ b/Pal.Client/Floors/PersistentLocation.cs @@ -44,5 +44,10 @@ namespace Pal.Client.Floors { return !Equals(a, b); } + + 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 017f96d..c224fee 100644 --- a/Pal.Client/Floors/Tasks/DbTask.cs +++ b/Pal.Client/Floors/Tasks/DbTask.cs @@ -1,10 +1,13 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Database; namespace Pal.Client.Floors.Tasks { - internal abstract class DbTask + internal abstract class DbTask + where T : DbTask { private readonly IServiceScopeFactory _serviceScopeFactory; @@ -18,12 +21,13 @@ namespace Pal.Client.Floors.Tasks Task.Run(() => { using var scope = _serviceScopeFactory.CreateScope(); + ILogger logger = scope.ServiceProvider.GetRequiredService>(); using var dbContext = scope.ServiceProvider.GetRequiredService(); - Run(dbContext); + Run(dbContext, logger); }); } - protected abstract void Run(PalClientContext dbContext); + 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 caa4291..b9f0958 100644 --- a/Pal.Client/Floors/Tasks/LoadTerritory.cs +++ b/Pal.Client/Floors/Tasks/LoadTerritory.cs @@ -4,11 +4,12 @@ using System.Linq; using System.Numerics; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Database; namespace Pal.Client.Floors.Tasks { - internal sealed class LoadTerritory : DbTask + internal sealed class LoadTerritory : DbTask { private readonly MemoryTerritory _territory; @@ -18,19 +19,27 @@ namespace Pal.Client.Floors.Tasks _territory = territory; } - protected override void Run(PalClientContext dbContext) + protected override void Run(PalClientContext dbContext, ILogger logger) { lock (_territory.LockObj) { if (_territory.IsReady) + { + logger.LogInformation("Territory {Territory} is already loaded", _territory.TerritoryType); return; + } + logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType); List 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); } } diff --git a/Pal.Client/Floors/Tasks/MarkAsSeen.cs b/Pal.Client/Floors/Tasks/MarkLocalSeen.cs similarity index 64% rename from Pal.Client/Floors/Tasks/MarkAsSeen.cs rename to Pal.Client/Floors/Tasks/MarkLocalSeen.cs index 3e1b767..c2f4dd7 100644 --- a/Pal.Client/Floors/Tasks/MarkAsSeen.cs +++ b/Pal.Client/Floors/Tasks/MarkLocalSeen.cs @@ -2,16 +2,17 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Database; namespace Pal.Client.Floors.Tasks { - internal sealed class MarkAsSeen : DbTask + internal sealed class MarkLocalSeen : DbTask { private readonly MemoryTerritory _territory; private readonly IReadOnlyList _locations; - public MarkAsSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, + public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, IReadOnlyList locations) : base(serviceScopeFactory) { @@ -19,10 +20,12 @@ namespace Pal.Client.Floors.Tasks _locations = locations; } - protected override void Run(PalClientContext dbContext) + protected override void Run(PalClientContext dbContext, ILogger logger) { lock (_territory.LockObj) { + logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}", _locations.Count, + _territory.TerritoryType); dbContext.Locations .Where(loc => _locations.Any(l => l.LocalId == loc.LocalId)) .ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true)); diff --git a/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs b/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs index 16a20c8..7a63741 100644 --- a/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs +++ b/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Database; namespace Pal.Client.Floors.Tasks { - internal sealed class MarkRemoteSeen : DbTask + internal sealed class MarkRemoteSeen : DbTask { private readonly MemoryTerritory _territory; private readonly IReadOnlyList _locations; @@ -22,16 +24,26 @@ namespace Pal.Client.Floors.Tasks _accountId = accountId; } - protected override void Run(PalClientContext dbContext) + protected override void Run(PalClientContext dbContext, ILogger logger) { lock (_territory.LockObj) { - List locationsToUpdate = dbContext.Locations - .Where(loc => _locations.Any(l => - l.LocalId == loc.LocalId && loc.RemoteEncounters.All(r => r.AccountId != _accountId))) - .ToList(); + 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(); } } diff --git a/Pal.Client/Floors/Tasks/SaveNewLocations.cs b/Pal.Client/Floors/Tasks/SaveNewLocations.cs index 4489740..5d1dd1d 100644 --- a/Pal.Client/Floors/Tasks/SaveNewLocations.cs +++ b/Pal.Client/Floors/Tasks/SaveNewLocations.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pal.Client.Database; using Pal.Common; namespace Pal.Client.Floors.Tasks { - internal sealed class SaveNewLocations : DbTask + internal sealed class SaveNewLocations : DbTask { private readonly MemoryTerritory _territory; private readonly List _newLocations; @@ -20,16 +21,22 @@ namespace Pal.Client.Floors.Tasks _newLocations = newLocations; } - protected override void Run(PalClientContext dbContext) + protected override void Run(PalClientContext dbContext, ILogger logger) { - Run(_territory, dbContext, _newLocations); + Run(_territory, dbContext, logger, _newLocations); } - public static void Run(MemoryTerritory territory, PalClientContext dbContext, + public static void Run( + MemoryTerritory territory, + PalClientContext dbContext, + ILogger logger, List locations) { lock (territory.LockObj) { + 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); diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index a62fe1b..98842e2 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -11,13 +11,15 @@ using System.Threading.Tasks; using Pal.Client.Extensions; using Pal.Client.Properties; using Pal.Client.Configuration; -using Pal.Client.DependencyInjection; namespace Pal.Client.Net { internal partial class RemoteApi { - private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) + private readonly SemaphoreSlim connectLock = new(1, 1); + + private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, + ILoggerFactory? loggerFactory = null, bool retry = true) { using IDisposable? logScope = _logger.BeginScope("TryConnect"); @@ -27,7 +29,8 @@ namespace Pal.Client.Net return (false, Localization.ConnectionError_NotOnline); } - if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) + if (_channel == null || + !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) { Dispose(); @@ -48,97 +51,122 @@ namespace Pal.Client.Net cancellationToken.ThrowIfCancellationRequested(); - var accountClient = new AccountService.AccountServiceClient(_channel); - IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl); - if (configuredAccount == null) + _logger.LogTrace("Acquiring connect lock"); + await connectLock.WaitAsync(cancellationToken); + _logger.LogTrace("Obtained connect lock"); + + try { - _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) + var accountClient = new AccountService.AccountServiceClient(_channel); + IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl); + if (configuredAccount == null) { - 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) + _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) { - _chat.Error(Localization.ConnectionError_OldVersion); - _warnedAboutUpgrade = true; - } - return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error)); - } - } + if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) + throw new InvalidOperationException("invalid account id returned"); - cancellationToken.ThrowIfCancellationRequested(); + configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); + _logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId()); - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (configuredAccount == null) - { - _logger.LogWarning("No account to login with"); - return (false, Localization.ConnectionError_CreateAccountReturnedNoId); - } - - if (!_loginInfo.IsValid) - { - _logger.LogInformation("Logging in with account id {AccountId}", configuredAccount.AccountId.ToPartialId()); - LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = configuredAccount.AccountId.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); - if (loginReply.Success) - { - _logger.LogInformation("Login successful with account id: {AccountId}", configuredAccount.AccountId.ToPartialId()); - _loginInfo = new LoginInfo(loginReply.AuthToken); - - bool save = configuredAccount.EncryptIfNeeded(); - - List newRoles = _loginInfo.Claims?.Roles.ToList() ?? new(); - if (!newRoles.SequenceEqual(configuredAccount.CachedRoles)) - { - configuredAccount.CachedRoles = newRoles; - save = true; - } - - if (save) _configurationManager.Save(_configuration); - } - else - { - _logger.LogError("Login failed with error {Error}", loginReply.Error); - _loginInfo = new LoginInfo(null); - if (loginReply.Error == LoginError.InvalidAccountId) + } + else { - _configuration.RemoveAccount(RemoteUrl); - _configurationManager.Save(_configuration); - if (retry) + _logger.LogError("Account creation failed with error {Error}", createAccountReply.Error); + if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) { - _logger.LogInformation("Attempting connection retry without account id"); - return await TryConnect(cancellationToken, retry: false); + _chat.Error(Localization.ConnectionError_OldVersion); + _warnedAboutUpgrade = true; } - else - return (false, Localization.ConnectionError_InvalidAccountId); + + return (false, + string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error)); } - if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) - { - _chat.Error(Localization.ConnectionError_OldVersion); - _warnedAboutUpgrade = true; - } - return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error)); } - } - if (!_loginInfo.IsValid) + cancellationToken.ThrowIfCancellationRequested(); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (configuredAccount == null) + { + _logger.LogWarning("No account to login with"); + return (false, Localization.ConnectionError_CreateAccountReturnedNoId); + } + + if (!_loginInfo.IsValid) + { + _logger.LogInformation("Logging in with account id {AccountId}", + configuredAccount.AccountId.ToPartialId()); + LoginReply loginReply = await accountClient.LoginAsync( + new LoginRequest { AccountId = configuredAccount.AccountId.ToString() }, + headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), + cancellationToken: cancellationToken); + + if (loginReply.Success) + { + _logger.LogInformation("Login successful with account id: {AccountId}", + configuredAccount.AccountId.ToPartialId()); + _loginInfo = new LoginInfo(loginReply.AuthToken); + + bool save = configuredAccount.EncryptIfNeeded(); + + List newRoles = _loginInfo.Claims?.Roles.ToList() ?? new(); + if (!newRoles.SequenceEqual(configuredAccount.CachedRoles)) + { + configuredAccount.CachedRoles = newRoles; + save = true; + } + + if (save) + _configurationManager.Save(_configuration); + } + else + { + _logger.LogError("Login failed with error {Error}", loginReply.Error); + _loginInfo = new LoginInfo(null); + if (loginReply.Error == LoginError.InvalidAccountId) + { + _configuration.RemoveAccount(RemoteUrl); + _configurationManager.Save(_configuration); + if (retry) + { + _logger.LogInformation("Attempting connection retry without account id"); + return await TryConnect(cancellationToken, retry: false); + } + else + return (false, Localization.ConnectionError_InvalidAccountId); + } + + if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) + { + _chat.Error(Localization.ConnectionError_OldVersion); + _warnedAboutUpgrade = true; + } + + return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error)); + } + } + + if (!_loginInfo.IsValid) + { + _logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, + _loginInfo.IsExpired); + return (false, Localization.ConnectionError_LoginReturnedNoToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + return (true, string.Empty); + } + finally { - _logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, _loginInfo.IsExpired); - return (false, Localization.ConnectionError_LoginReturnedNoToken); + _logger.LogTrace("Releasing connectLock"); + connectLock.Release(); } - - cancellationToken.ThrowIfCancellationRequested(); - return (true, string.Empty); } private async Task Connect(CancellationToken cancellationToken) @@ -159,7 +187,8 @@ namespace Pal.Client.Net _logger.LogInformation("Connection established, trying to verify auth token"); var accountClient = new AccountService.AccountServiceClient(_channel); - await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), + deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); _logger.LogInformation("Verification returned no errors."); return Localization.ConnectionSuccessful; @@ -182,7 +211,10 @@ namespace Pal.Client.Net public bool IsLoggedIn { get; } public string? AuthToken { get; } public JwtClaims? Claims { get; } - private DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; + + private DateTimeOffset ExpiresAt => + Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; + public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow; public bool IsValid => IsLoggedIn && !IsExpired; diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs index cb4dbde..b706ecc 100644 --- a/Pal.Client/Net/RemoteApi.PalaceService.cs +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -36,7 +36,7 @@ namespace Pal.Client.Net }; uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject { - Type = (ObjectType)m.Type, + Type = m.Type.ToObjectType(), X = m.Position.X, Y = m.Position.Y, Z = m.Position.Z diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 6815210..70654da 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -11,7 +11,7 @@ namespace Pal.Client.Net internal sealed partial class RemoteApi : IDisposable { #if DEBUG - public const string RemoteUrl = "http://localhost:5145"; + public const string RemoteUrl = "http://localhost:5415"; #else //public const string RemoteUrl = "https://pal.liza.sh"; #endif diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 62c5adf..c4b3503 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -36,7 +36,7 @@ namespace Pal.Client private readonly IServiceScope _rootScope; private readonly DependencyInjectionLoader _loader; - private Action? _loginAction = null; + private Action? _loginAction; public Plugin( DalamudPluginInterface pluginInterface, diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index beed1aa..519f5f1 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -9,6 +9,7 @@ using Pal.Client.Extensions; using Pal.Client.Floors; using Pal.Client.Floors.Tasks; using Pal.Client.Net; +using Pal.Common; namespace Pal.Client.Scheduled { @@ -47,12 +48,21 @@ namespace Pal.Client.Scheduled { recreateLayout = true; + _logger.LogDebug( + "Sync response for territory {Territory} of type {Type}, success = {Success}, response objects = {Count}", + (ETerritoryType)queued.TerritoryType, queued.Type, queued.Success, queued.Locations.Count); + var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType); + if (memoryTerritory == null) + { + _logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready", + (ETerritoryType)queued.TerritoryType); + return; + } + try { var remoteMarkers = queued.Locations; - var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType); - if (memoryTerritory != null && _configuration.Mode == EMode.Online && queued.Success && - remoteMarkers.Count > 0) + if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) { switch (queued.Type) { @@ -117,16 +127,17 @@ namespace Pal.Client.Scheduled if (queued.Type == SyncType.Download) { if (queued.Success) - _territoryState.TerritorySyncState = ESyncState.Complete; + memoryTerritory.SyncState = ESyncState.Complete; else - _territoryState.TerritorySyncState = ESyncState.Failed; + memoryTerritory.SyncState = ESyncState.Failed; } } catch (Exception e) { + _logger.LogError(e, "Sync failed for territory {Territory}", (ETerritoryType)queued.TerritoryType); _debugState.SetFromException(e); if (queued.Type == SyncType.Download) - _territoryState.TerritorySyncState = ESyncState.Failed; + memoryTerritory.SyncState = ESyncState.Failed; } } } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 77b6e13..4e68c62 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -375,12 +375,12 @@ namespace Pal.Client.Windows { if (_territoryState.IsInDeepDungeon()) { + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); ImGui.Text($"You are in a deep dungeon, territory type {_territoryState.LastTerritory}."); - ImGui.Text($"Sync State = {_territoryState.TerritorySyncState}"); + ImGui.Text($"Sync State = {memoryTerritory?.SyncState.ToString() ?? "Unknown"}"); ImGui.Text($"{_debugState.DebugMessage}"); ImGui.Indent(); - MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); if (memoryTerritory != null) { if (_trapConfig.Show) From 7bccec0bae93d9b09d70343114934bd45a0566c6 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 22 Feb 2023 20:29:58 +0100 Subject: [PATCH 32/38] Db: lmport --- .../Configuration/Legacy/JsonMigration.cs | 5 +- Pal.Client/Database/Cleanup.cs | 67 +++++++ Pal.Client/Database/ClientLocation.cs | 13 +- ...ChangeLocationImportedToSource.Designer.cs | 148 ++++++++++++++ ...22191929_ChangeLocationImportedToSource.cs | 28 +++ .../PalClientContextModelSnapshot.cs | 6 +- .../DependencyInjection/FrameworkService.cs | 11 +- .../DependencyInjection/ImportService.cs | 180 ++++++++++++------ Pal.Client/DependencyInjectionContext.cs | 1 + Pal.Client/DependencyInjectionLoader.cs | 30 ++- Pal.Client/Floors/FloorService.cs | 26 ++- Pal.Client/Floors/MemoryTerritory.cs | 36 ++-- Pal.Client/Floors/PersistentLocation.cs | 2 + Pal.Client/Floors/Tasks/DbTask.cs | 3 +- Pal.Client/Floors/Tasks/LoadTerritory.cs | 17 +- Pal.Client/Floors/Tasks/SaveNewLocations.cs | 1 + Pal.Client/Net/RemoteApi.PalaceService.cs | 2 + Pal.Client/Net/RemoteApi.cs | 2 +- Pal.Client/Scheduled/QueuedImport.cs | 35 ++-- Pal.Client/Windows/ConfigWindow.cs | 5 +- Pal.Common/ETerritoryType.cs | 3 + 21 files changed, 505 insertions(+), 116 deletions(-) create mode 100644 Pal.Client/Database/Cleanup.cs create mode 100644 Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.Designer.cs create mode 100644 Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.cs diff --git a/Pal.Client/Configuration/Legacy/JsonMigration.cs b/Pal.Client/Configuration/Legacy/JsonMigration.cs index c657043..c9b9a6a 100644 --- a/Pal.Client/Configuration/Legacy/JsonMigration.cs +++ b/Pal.Client/Configuration/Legacy/JsonMigration.cs @@ -115,7 +115,10 @@ namespace Pal.Client.Configuration.Legacy .Distinct() .ToList(), - Imported = o.WasImported, + // 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", }; diff --git a/Pal.Client/Database/Cleanup.cs b/Pal.Client/Database/Cleanup.cs new file mode 100644 index 0000000..40db00f --- /dev/null +++ b/Pal.Client/Database/Cleanup.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Pal.Client.Configuration; +using Pal.Common; + +namespace Pal.Client.Database +{ + internal sealed class Cleanup + { + private readonly ILogger _logger; + private readonly IPalacePalConfiguration _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, 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> 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 e545edd..ab748f5 100644 --- a/Pal.Client/Database/ClientLocation.cs +++ b/Pal.Client/Database/ClientLocation.cs @@ -31,9 +31,9 @@ namespace Pal.Client.Database public List ImportedBy { get; set; } = new(); /// - /// Whether this location was originally imported. + /// Determines where this location is originally from. /// - public bool Imported { get; set; } + public ESource Source { get; set; } /// @@ -46,5 +46,14 @@ namespace Pal.Client.Database Trap = 1, Hoard = 2, } + + public enum ESource + { + Unknown = 0, + SeenLocally = 1, + ExplodedLocally = 2, + Import = 3, + Download = 4, + } } } diff --git a/Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.Designer.cs b/Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.Designer.cs new file mode 100644 index 0000000..0924e17 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.Designer.cs @@ -0,0 +1,148 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Pal.Client.Database; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + [DbContext(typeof(PalClientContext))] + [Migration("20230222191929_ChangeLocationImportedToSource")] + partial class ChangeLocationImportedToSource + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.Property("ImportedById") + .HasColumnType("TEXT"); + + b.Property("ImportedLocationsLocalId") + .HasColumnType("INTEGER"); + + b.HasKey("ImportedById", "ImportedLocationsLocalId"); + + b.HasIndex("ImportedLocationsLocalId"); + + b.ToTable("LocationImports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Property("LocalId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.Property("SinceVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("TerritoryType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("LocalId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ExportedAt") + .HasColumnType("TEXT"); + + b.Property("ImportedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Imports"); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("TEXT"); + + b.Property("ClientLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClientLocationId"); + + b.ToTable("RemoteEncounters"); + }); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.HasOne("Pal.Client.Database.ImportHistory", null) + .WithMany() + .HasForeignKey("ImportedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Pal.Client.Database.ClientLocation", null) + .WithMany() + .HasForeignKey("ImportedLocationsLocalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") + .WithMany("RemoteEncounters") + .HasForeignKey("ClientLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClientLocation"); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Navigation("RemoteEncounters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.cs b/Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.cs new file mode 100644 index 0000000..20cf1e0 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + /// + public partial class ChangeLocationImportedToSource : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Imported", + table: "Locations", + newName: "Source"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Source", + table: "Locations", + newName: "Imported"); + } + } +} diff --git a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs index 963f393..55e0dff 100644 --- a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs +++ b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs @@ -38,9 +38,6 @@ namespace Pal.Client.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("Imported") - .HasColumnType("INTEGER"); - b.Property("Seen") .HasColumnType("INTEGER"); @@ -48,6 +45,9 @@ namespace Pal.Client.Database.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("Source") + .HasColumnType("INTEGER"); + b.Property("TerritoryType") .HasColumnType("INTEGER"); diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs index 0a8880e..781bf28 100644 --- a/Pal.Client/DependencyInjection/FrameworkService.cs +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -14,6 +14,7 @@ using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; +using Pal.Client.Database; using Pal.Client.Extensions; using Pal.Client.Floors; using Pal.Client.Net; @@ -390,7 +391,8 @@ namespace Pal.Client.DependencyInjection { Type = MemoryLocation.EType.Trap, Position = obj.Position, - Seen = true + Seen = true, + Source = ClientLocation.ESource.SeenLocally, }); break; @@ -400,7 +402,8 @@ namespace Pal.Client.DependencyInjection { Type = MemoryLocation.EType.Hoard, Position = obj.Position, - Seen = true + Seen = true, + Source = ClientLocation.ESource.SeenLocally, }); break; @@ -409,7 +412,7 @@ namespace Pal.Client.DependencyInjection { Type = MemoryLocation.EType.SilverCoffer, Position = obj.Position, - Seen = true + Seen = true, }); break; } @@ -425,6 +428,8 @@ namespace Pal.Client.DependencyInjection Type = MemoryLocation.EType.Trap, Position = obj.Position, Seen = true, + Source = ClientLocation.ESource.ExplodedLocally, + }); } } diff --git a/Pal.Client/DependencyInjection/ImportService.cs b/Pal.Client/DependencyInjection/ImportService.cs index 4d074dd..d704afb 100644 --- a/Pal.Client/DependencyInjection/ImportService.cs +++ b/Pal.Client/DependencyInjection/ImportService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Threading; using System.Threading.Tasks; using Account; @@ -17,94 +18,149 @@ namespace Pal.Client.DependencyInjection { private readonly IServiceProvider _serviceProvider; private readonly FloorService _floorService; + private readonly Cleanup _cleanup; - public ImportService(IServiceProvider serviceProvider, FloorService floorService) + public ImportService( + IServiceProvider serviceProvider, + FloorService floorService, + Cleanup cleanup) { _serviceProvider = serviceProvider; _floorService = floorService; + _cleanup = cleanup; } - /* - public void Add(ImportHistory history) - { - using var scope = _serviceProvider.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - - dbContext.Imports.Add(history); - dbContext.SaveChanges(); - } - */ - 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); + return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id) + .FirstOrDefaultAsync(cancellationToken: token); } - /* - public List FindForServer(string server) - { - if (string.IsNullOrEmpty(server)) - return new(); - - using var scope = _serviceProvider.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - - return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList(); - }*/ - public (int traps, int hoard) Import(ExportRoot import) { - using var scope = _serviceProvider.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - - dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList()); - - ImportHistory importHistory = new ImportHistory + try { - Id = Guid.Parse(import.ExportId), - RemoteUrl = import.ServerUrl, - ExportedAt = import.CreatedAt.ToDateTime(), - ImportedAt = DateTime.UtcNow, - }; - dbContext.Imports.Add(importHistory); + _floorService.SetToImportState(); - int traps = 0; - int hoard = 0; - foreach (var floor in import.Floors) - { - ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType; + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); - List existingLocations = dbContext.Locations - .Where(loc => loc.TerritoryType == floor.TerritoryType) - .ToList() - .Select(LoadTerritory.ToMemoryLocation) - .ToList(); - foreach (var newLocation in floor.Objects) + dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList()); + dbContext.SaveChanges(); + + ImportHistory importHistory = new ImportHistory { - throw new NotImplementedException(); - } - } - // TODO filter here, update territories - dbContext.SaveChanges(); + Id = Guid.Parse(import.ExportId), + RemoteUrl = import.ServerUrl, + ExportedAt = import.CreatedAt.ToDateTime(), + ImportedAt = DateTime.UtcNow, + }; + dbContext.Imports.Add(importHistory); - _floorService.ResetAll(); - return (traps, hoard); + 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 + { + 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); + + 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(); + } + } + + private MemoryLocation.EType ToMemoryType(ExportObjectType exportLocationType) + { + return exportLocationType switch + { + 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) { - using var scope = _serviceProvider.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); + try + { + _floorService.SetToImportState(); + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id)); + dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id)); + dbContext.SaveChanges(); - // TODO filter here, update territories - dbContext.SaveChanges(); - - _floorService.ResetAll(); + _cleanup.Purge(dbContext); + dbContext.SaveChanges(); + } + finally + { + _floorService.ResetAll(); + } } } } diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 33d9724..1ce57e5 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -96,6 +96,7 @@ namespace Pal.Client $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; services.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); services.AddTransient(); + services.AddScoped(); // plugin-specific services.AddScoped(); diff --git a/Pal.Client/DependencyInjectionLoader.cs b/Pal.Client/DependencyInjectionLoader.cs index 5a0eec1..9466eab 100644 --- a/Pal.Client/DependencyInjectionLoader.cs +++ b/Pal.Client/DependencyInjectionLoader.cs @@ -55,7 +55,9 @@ namespace Pal.Client cancellationToken.ThrowIfCancellationRequested(); await RunMigrations(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + await RunCleanup(_logger); cancellationToken.ThrowIfCancellationRequested(); // v1 migration: config migration for import history, json migration for markers @@ -174,18 +176,28 @@ namespace Pal.Client private async Task RunMigrations(CancellationToken cancellationToken) { - // initialize database - await using (var scope = _serviceProvider.CreateAsyncScope()) - { - _logger.LogInformation("Loading database & running migrations"); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); + await using var scope = _serviceProvider.CreateAsyncScope(); - // takes 2-3 seconds with initializing connections, loading driver etc. - await dbContext.Database.MigrateAsync(cancellationToken); - _logger.LogInformation("Completed database migrations"); - } + _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(ILogger logger) + { + await using var scope = _serviceProvider.CreateAsyncScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + var cleanup = scope.ServiceProvider.GetRequiredService(); + + cleanup.Purge(dbContext); + + await dbContext.SaveChangesAsync(); + } + + public enum ELoadState { Initializing, diff --git a/Pal.Client/Floors/FloorService.cs b/Pal.Client/Floors/FloorService.cs index 8b53d75..bfb2b58 100644 --- a/Pal.Client/Floors/FloorService.cs +++ b/Pal.Client/Floors/FloorService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Pal.Client.Configuration; +using Pal.Client.Database; using Pal.Client.Extensions; using Pal.Client.Floors.Tasks; using Pal.Client.Net; @@ -14,19 +15,23 @@ namespace Pal.Client.Floors 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, IServiceScopeFactory serviceScopeFactory) + public FloorService(IPalacePalConfiguration configuration, Cleanup cleanup, + IServiceScopeFactory serviceScopeFactory) { _configuration = configuration; + _cleanup = cleanup; _serviceScopeFactory = serviceScopeFactory; _territories = Enum.GetValues().ToDictionary(o => o, o => new MemoryTerritory(o)); } public IReadOnlyCollection EphemeralLocations => _ephemeralLocations; + public bool IsImportRunning { get; private set; } public void ChangeTerritory(ushort territoryType) { @@ -39,10 +44,10 @@ namespace Pal.Client.Floors private void ChangeTerritory(ETerritoryType newTerritory) { var territory = _territories[newTerritory]; - if (!territory.IsReady && !territory.IsLoading) + if (territory.ReadyState == MemoryTerritory.EReadyState.NotLoaded) { - territory.IsLoading = true; - new LoadTerritory(_serviceScopeFactory, territory).Start(); + territory.ReadyState = MemoryTerritory.EReadyState.Loading; + new LoadTerritory(_serviceScopeFactory, _cleanup, territory).Start(); } } @@ -57,7 +62,7 @@ namespace Pal.Client.Floors public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType) { var territory = _territories[territoryType]; - if (!territory.IsReady) + if (territory.ReadyState != MemoryTerritory.EReadyState.Ready) return null; return territory; @@ -137,11 +142,22 @@ namespace Pal.Client.Floors public void ResetAll() { + 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; + } + } } } diff --git a/Pal.Client/Floors/MemoryTerritory.cs b/Pal.Client/Floors/MemoryTerritory.cs index d0708ae..e440bc8 100644 --- a/Pal.Client/Floors/MemoryTerritory.cs +++ b/Pal.Client/Floors/MemoryTerritory.cs @@ -18,8 +18,7 @@ namespace Pal.Client.Floors } public ETerritoryType TerritoryType { get; } - public bool IsReady { get; set; } - public bool IsLoading { get; set; } // probably merge this with IsReady as enum + public EReadyState ReadyState { get; set; } = EReadyState.NotLoaded; public ESyncState SyncState { get; set; } = ESyncState.NotAttempted; public ConcurrentBag Locations { get; } = new(); @@ -31,21 +30,34 @@ namespace Pal.Client.Floors foreach (var location in locations) Locations.Add(location); - IsReady = true; - IsLoading = false; - } - - public IEnumerable GetRemovableLocations(EMode mode) - { - // TODO there was better logic here; - return Locations.Where(x => !x.Seen); + ReadyState = EReadyState.Ready; } public void Reset() { Locations.Clear(); - IsReady = false; - IsLoading = false; + SyncState = ESyncState.NotAttempted; + ReadyState = EReadyState.NotLoaded; + } + + public enum EReadyState + { + NotLoaded, + + /// + /// Currently loading from the database. + /// + Loading, + + /// + /// Locations loaded, no import running. + /// + Ready, + + /// + /// Import running, should probably not interact with this too much. + /// + Importing, } } } diff --git a/Pal.Client/Floors/PersistentLocation.cs b/Pal.Client/Floors/PersistentLocation.cs index 99cc5dd..e6f8ad6 100644 --- a/Pal.Client/Floors/PersistentLocation.cs +++ b/Pal.Client/Floors/PersistentLocation.cs @@ -31,6 +31,8 @@ namespace Pal.Client.Floors /// 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(); diff --git a/Pal.Client/Floors/Tasks/DbTask.cs b/Pal.Client/Floors/Tasks/DbTask.cs index c224fee..64074fe 100644 --- a/Pal.Client/Floors/Tasks/DbTask.cs +++ b/Pal.Client/Floors/Tasks/DbTask.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; diff --git a/Pal.Client/Floors/Tasks/LoadTerritory.cs b/Pal.Client/Floors/Tasks/LoadTerritory.cs index b9f0958..7e11b2f 100644 --- a/Pal.Client/Floors/Tasks/LoadTerritory.cs +++ b/Pal.Client/Floors/Tasks/LoadTerritory.cs @@ -11,11 +11,15 @@ namespace Pal.Client.Floors.Tasks { internal sealed class LoadTerritory : DbTask { + private readonly Cleanup _cleanup; private readonly MemoryTerritory _territory; - public LoadTerritory(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory) + public LoadTerritory(IServiceScopeFactory serviceScopeFactory, + Cleanup cleanup, + MemoryTerritory territory) : base(serviceScopeFactory) { + _cleanup = cleanup; _territory = territory; } @@ -23,13 +27,19 @@ namespace Pal.Client.Floors.Tasks { lock (_territory.LockObj) { - if (_territory.IsReady) + if (_territory.ReadyState != MemoryTerritory.EReadyState.Loading) { - logger.LogInformation("Territory {Territory} is already loaded", _territory.TerritoryType); + 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) @@ -51,6 +61,7 @@ namespace Pal.Client.Floors.Tasks 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(), }; } diff --git a/Pal.Client/Floors/Tasks/SaveNewLocations.cs b/Pal.Client/Floors/Tasks/SaveNewLocations.cs index 5d1dd1d..345986a 100644 --- a/Pal.Client/Floors/Tasks/SaveNewLocations.cs +++ b/Pal.Client/Floors/Tasks/SaveNewLocations.cs @@ -59,6 +59,7 @@ namespace Pal.Client.Floors.Tasks Y = location.Position.Y, Z = location.Position.Z, Seen = location.Seen, + Source = location.Source, SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2), }; } diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs index b706ecc..161b3f5 100644 --- a/Pal.Client/Net/RemoteApi.PalaceService.cs +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; +using Pal.Client.Database; using Pal.Client.Floors; namespace Pal.Client.Net @@ -69,6 +70,7 @@ namespace Pal.Client.Net Type = obj.Type.ToMemoryType(), Position = new Vector3(obj.X, obj.Y, obj.Z), NetworkId = Guid.Parse(obj.NetworkId), + Source = ClientLocation.ESource.Download, }; } diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 70654da..7f94781 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -13,7 +13,7 @@ namespace Pal.Client.Net #if DEBUG public const string RemoteUrl = "http://localhost:5415"; #else - //public const string RemoteUrl = "https://pal.liza.sh"; + public const string RemoteUrl = "http://localhost:5415"; #endif private readonly string _userAgent = $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 89a947b..8c07149 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -2,11 +2,11 @@ using Pal.Common; using System; using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; using Pal.Client.DependencyInjection; -using Pal.Client.Floors; using Pal.Client.Properties; using Pal.Client.Windows; @@ -55,20 +55,31 @@ namespace Pal.Client.Scheduled if (!Validate(import)) return; - - using (var scope = _serviceScopeFactory.CreateScope()) + Task.Run(() => { - using var dbContext = scope.ServiceProvider.GetRequiredService(); - (import.ImportedTraps, import.ImportedHoardCoffers) = _importService.Import(import.Export); - } + try + { + using (var scope = _serviceScopeFactory.CreateScope()) + { + using var dbContext = scope.ServiceProvider.GetRequiredService(); + (import.ImportedTraps, import.ImportedHoardCoffers) = + _importService.Import(import.Export); + } - _configWindow.UpdateLastImport(); + _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)); + _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)); + } + }); } catch (Exception e) { diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 4e68c62..c3b279b 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -284,7 +284,7 @@ namespace Pal.Client.Windows null; // only use this once, FileDialogManager will save path between calls } - ImGui.BeginDisabled(string.IsNullOrEmpty(_openImportPath) || !File.Exists(_openImportPath)); + ImGui.BeginDisabled(string.IsNullOrEmpty(_openImportPath) || !File.Exists(_openImportPath) || _floorService.IsImportRunning); if (ImGui.Button(Localization.Config_StartImport)) DoImport(_openImportPath); ImGui.EndDisabled(); @@ -298,8 +298,11 @@ namespace Pal.Client.Windows 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(); diff --git a/Pal.Common/ETerritoryType.cs b/Pal.Common/ETerritoryType.cs index 9796f86..99e3ce6 100644 --- a/Pal.Common/ETerritoryType.cs +++ b/Pal.Common/ETerritoryType.cs @@ -1,7 +1,10 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Pal.Common { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + [SuppressMessage("ReSharper", "InconsistentNaming")] public enum ETerritoryType : ushort { Palace_1_10 = 561, From dbe6abd1db5054488edbe25841ca7366efb3d12b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 22 Feb 2023 21:54:33 +0100 Subject: [PATCH 33/38] Net: Change retry logic --- Pal.Client/Net/RemoteApi.AccountService.cs | 41 +++++++++++++--------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index 98842e2..c76554e 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -16,17 +16,28 @@ namespace Pal.Client.Net { internal partial class RemoteApi { - private readonly SemaphoreSlim connectLock = new(1, 1); + private readonly SemaphoreSlim _connectLock = new(1, 1); private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) { using IDisposable? logScope = _logger.BeginScope("TryConnect"); + 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) { _logger.LogDebug("Not Online, not attempting to establish a connection"); - return (false, Localization.ConnectionError_NotOnline); + return (false, Localization.ConnectionError_NotOnline, false); } if (_channel == null || @@ -52,7 +63,7 @@ namespace Pal.Client.Net cancellationToken.ThrowIfCancellationRequested(); _logger.LogTrace("Acquiring connect lock"); - await connectLock.WaitAsync(cancellationToken); + await _connectLock.WaitAsync(cancellationToken); _logger.LogTrace("Obtained connect lock"); try @@ -85,7 +96,8 @@ namespace Pal.Client.Net } return (false, - string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error)); + string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error), + false); } } @@ -95,7 +107,7 @@ namespace Pal.Client.Net if (configuredAccount == null) { _logger.LogWarning("No account to login with"); - return (false, Localization.ConnectionError_CreateAccountReturnedNoId); + return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false); } if (!_loginInfo.IsValid) @@ -133,13 +145,9 @@ namespace Pal.Client.Net { _configuration.RemoveAccount(RemoteUrl); _configurationManager.Save(_configuration); - if (retry) - { - _logger.LogInformation("Attempting connection retry without account id"); - return await TryConnect(cancellationToken, retry: false); - } - else - return (false, Localization.ConnectionError_InvalidAccountId); + + _logger.LogInformation("Attempting connection retry without account id"); + return (false, Localization.ConnectionError_InvalidAccountId, true); } if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) @@ -148,7 +156,8 @@ namespace Pal.Client.Net _warnedAboutUpgrade = true; } - return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error)); + return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error), + false); } } @@ -156,16 +165,16 @@ namespace Pal.Client.Net { _logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, _loginInfo.IsExpired); - return (false, Localization.ConnectionError_LoginReturnedNoToken); + return (false, Localization.ConnectionError_LoginReturnedNoToken, false); } cancellationToken.ThrowIfCancellationRequested(); - return (true, string.Empty); + return (true, string.Empty, false); } finally { _logger.LogTrace("Releasing connectLock"); - connectLock.Release(); + _connectLock.Release(); } } From 8d17c02186176b3d67cbb4317ac10d3d7e85a0cd Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 22 Feb 2023 22:20:50 +0100 Subject: [PATCH 34/38] DI: Load entire DI container in the background --- ...der.cs => DependencyContextInitializer.cs} | 91 +++---- Pal.Client/DependencyInjectionContext.cs | 152 ++++++------ Pal.Client/Hooks.cs | 6 +- Pal.Client/Plugin.cs | 232 ++++++++++++------ .../Scheduled/IQueueOnFrameworkThread.cs | 2 +- 5 files changed, 258 insertions(+), 225 deletions(-) rename Pal.Client/{DependencyInjectionLoader.cs => DependencyContextInitializer.cs} (64%) diff --git a/Pal.Client/DependencyInjectionLoader.cs b/Pal.Client/DependencyContextInitializer.cs similarity index 64% rename from Pal.Client/DependencyInjectionLoader.cs rename to Pal.Client/DependencyContextInitializer.cs index 9466eab..db1c679 100644 --- a/Pal.Client/DependencyInjectionLoader.cs +++ b/Pal.Client/DependencyContextInitializer.cs @@ -16,7 +16,6 @@ using Pal.Client.Configuration; using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using Pal.Client.DependencyInjection; -using Pal.Client.Properties; using Pal.Client.Windows; namespace Pal.Client @@ -25,80 +24,56 @@ 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 DependencyInjectionLoader + internal sealed class DependencyContextInitializer { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; - public DependencyInjectionLoader(ILogger logger, IServiceProvider serviceProvider) + public DependencyContextInitializer(ILogger logger, IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; } - public ELoadState LoadState { get; private set; } = ELoadState.Initializing; - - public event Action? InitCompleted; - public async Task InitializeAsync(CancellationToken cancellationToken) { using IDisposable? logScope = _logger.BeginScope("AsyncInit"); - Chat? chat = null; - try - { - _logger.LogInformation("Starting async init"); - chat = _serviceProvider.GetService(); + _logger.LogInformation("Starting async init"); - await RemoveOldBackups(); - await CreateBackups(); - cancellationToken.ThrowIfCancellationRequested(); + await RemoveOldBackups(); + await CreateBackups(); + cancellationToken.ThrowIfCancellationRequested(); - await RunMigrations(cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + await RunMigrations(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - await RunCleanup(_logger); - cancellationToken.ThrowIfCancellationRequested(); + await RunCleanup(); + cancellationToken.ThrowIfCancellationRequested(); - // v1 migration: config migration for import history, json migration for markers - _serviceProvider.GetRequiredService().Migrate(); - await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); + // v1 migration: config migration for import history, json migration for markers + _serviceProvider.GetRequiredService().Migrate(); + await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + 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(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); + // eager load any commands to find errors now, not when running them + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - LoadState = ELoadState.Loaded; - InitCompleted?.Invoke(null); - _logger.LogInformation("Async init complete"); - } - catch (ObjectDisposedException) - { - InitCompleted?.Invoke(null); - LoadState = ELoadState.Error; - } - catch (Exception e) - { - _logger.LogError(e, "Async load failed"); - InitCompleted?.Invoke(() => - chat?.Error(string.Format(Localization.Error_LoadFailed, $"{e.GetType()} - {e.Message}"))); - - LoadState = ELoadState.Error; - } + _logger.LogInformation("Async init complete"); } private async Task RemoveOldBackups() @@ -186,7 +161,7 @@ namespace Pal.Client _logger.LogInformation("Completed database migrations"); } - private async Task RunCleanup(ILogger logger) + private async Task RunCleanup() { await using var scope = _serviceProvider.CreateAsyncScope(); await using var dbContext = scope.ServiceProvider.GetRequiredService(); @@ -196,13 +171,5 @@ namespace Pal.Client await dbContext.SaveChangesAsync(); } - - - public enum ELoadState - { - Initializing, - Loaded, - Error - } } } diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 1ce57e5..00c36cb 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -1,6 +1,5 @@ -using System.Globalization; +using System; using System.IO; -using System.Threading; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; @@ -32,20 +31,18 @@ namespace Pal.Client /// /// DI-aware Plugin. /// - // ReSharper disable once UnusedType.Global - internal sealed class DependencyInjectionContext : IDalamudPlugin + internal sealed class DependencyInjectionContext : IDisposable { public static DalamudLoggerProvider LoggerProvider { get; } = new(); /// /// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes. /// - private readonly ILogger _logger = LoggerProvider.CreateLogger(); + private ILogger _logger = LoggerProvider.CreateLogger(); private readonly string _sqliteConnectionString; - private readonly CancellationTokenSource _initCts = new(); + private readonly ServiceCollection _serviceCollection = new(); private ServiceProvider? _serviceProvider; - private Plugin? _plugin; public string Name => Localization.Palace_Pal; @@ -58,9 +55,10 @@ namespace Pal.Client Framework framework, Condition condition, CommandManager commandManager, - DataManager dataManager) + DataManager dataManager, + Plugin plugin) { - _logger.LogInformation("Building service container for {Assembly}", + _logger.LogInformation("Building dalamud service container for {Assembly}", typeof(DependencyInjectionContext).Assembly.FullName); // set up legacy services @@ -69,8 +67,7 @@ namespace Pal.Client #pragma warning restore CS0612 // set up logging - IServiceCollection services = new ServiceCollection(); - services.AddLogging(builder => + _serviceCollection.AddLogging(builder => builder.AddFilter("Pal", LogLevel.Trace) .AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) .AddFilter("Grpc", LogLevel.Debug) @@ -78,70 +75,78 @@ namespace Pal.Client .AddProvider(LoggerProvider)); // dalamud - services.AddSingleton(this); - services.AddSingleton(pluginInterface); - services.AddSingleton(clientState); - services.AddSingleton(gameGui); - services.AddSingleton(chatGui); - services.AddSingleton(); - services.AddSingleton(objectTable); - services.AddSingleton(framework); - services.AddSingleton(condition); - services.AddSingleton(commandManager); - services.AddSingleton(dataManager); - services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); + _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)); - // EF core _sqliteConnectionString = $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; - services.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); - services.AddTransient(); - services.AddScoped(); + } + + public IServiceProvider BuildServiceContainer() + { + _logger.LogInformation("Building async service container for {Assembly}", + typeof(DependencyInjectionContext).Assembly.FullName); + + // EF core + _serviceCollection.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); + _serviceCollection.AddTransient(); + _serviceCollection.AddScoped(); // plugin-specific - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(sp => sp.GetRequiredService().Load()); - services.AddTransient(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(sp => + sp.GetRequiredService().Load()); + _serviceCollection.AddTransient(); // commands - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // territory & marker related services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // windows & related services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // rendering - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // queue handling - services.AddTransient, QueuedImport.Handler>(); - services.AddTransient, QueuedUndoImport.Handler>(); - services.AddTransient, QueuedConfigUpdate.Handler>(); - services.AddTransient, QueuedSyncResponse.Handler>(); - - // set up the current UI language before creating anything - Localization.Culture = new CultureInfo(pluginInterface.UiLanguage); + _serviceCollection.AddTransient, QueuedImport.Handler>(); + _serviceCollection + .AddTransient, QueuedUndoImport.Handler>(); + _serviceCollection + .AddTransient, QueuedConfigUpdate.Handler>(); + _serviceCollection + .AddTransient, QueuedSyncResponse.Handler>(); // build - _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions + _serviceProvider = _serviceCollection.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, @@ -165,34 +170,19 @@ namespace Pal.Client // 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, creating plugin"); - _plugin = new Plugin(pluginInterface, _serviceProvider, _initCts.Token); + _logger.LogInformation("Service container built"); + + return _serviceProvider; } public void Dispose() { - _initCts.Cancel(); + _logger.LogInformation("Disposing DI Context"); + _serviceProvider?.Dispose(); - // ensure we're not calling dispose recursively on ourselves - if (_serviceProvider != null) - { - _logger.LogInformation("Disposing DI Context"); - - ServiceProvider serviceProvider = _serviceProvider; - _serviceProvider = null; - - _plugin?.Dispose(); - _plugin = null; - serviceProvider.Dispose(); - - // ensure we're not keeping the file open longer than the plugin is loaded - using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) - SqliteConnection.ClearPool(sqliteConnection); - } - else - { - _logger.LogDebug("DI context is already disposed"); - } + // 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/Hooks.cs b/Pal.Client/Hooks.cs index 17707e6..cf4b373 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -1,6 +1,5 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Memory; using Dalamud.Utility.Signatures; using System; @@ -32,8 +31,11 @@ namespace Pal.Client _territoryState = territoryState; _frameworkService = frameworkService; + _logger.LogDebug("Initializing game hooks"); SignatureHelper.Initialise(this); ActorVfxCreateHook.Enable(); + + _logger.LogDebug("Game hooks initialized"); } /// @@ -82,6 +84,7 @@ namespace Pal.Client { 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); } } @@ -96,6 +99,7 @@ namespace Pal.Client public void Dispose() { + _logger.LogDebug("Disposing game hooks"); ActorVfxCreateHook.Dispose(); } } diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index c4b3503..0adafc0 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -1,14 +1,15 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Pal.Client.Rendering; -using Pal.Client.Windows; using System; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Command; +using Dalamud.Game.Gui; using Pal.Client.Properties; using ECommons; using Microsoft.Extensions.DependencyInjection; @@ -24,52 +25,101 @@ namespace Pal.Client /// need to be sent to different receivers depending on priority or configuration . /// /// - internal sealed class Plugin : IDisposable + internal sealed class Plugin : IDalamudPlugin { + private readonly CancellationTokenSource _initCts = new(); + private readonly DalamudPluginInterface _pluginInterface; - private readonly ILogger _logger; private readonly CommandManager _commandManager; - private readonly Chat _chat; - private readonly WindowSystem _windowSystem; private readonly ClientState _clientState; + private readonly ChatGui _chatGui; + private readonly Framework _framework; - private readonly IServiceScope _rootScope; - private readonly DependencyInjectionLoader _loader; + 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, - IServiceProvider serviceProvider, - CancellationToken cancellationToken) + CommandManager commandManager, + ClientState clientState, + ChatGui chatGui, + Framework framework) { _pluginInterface = pluginInterface; - _logger = serviceProvider.GetRequiredService>(); - _commandManager = serviceProvider.GetRequiredService(); - _chat = serviceProvider.GetRequiredService(); - _windowSystem = serviceProvider.GetRequiredService(); - _clientState = serviceProvider.GetRequiredService(); + _commandManager = commandManager; + _clientState = clientState; + _chatGui = chatGui; + _framework = framework; - _rootScope = serviceProvider.CreateScope(); - _loader = _rootScope.ServiceProvider.GetRequiredService(); - _loader.InitCompleted += InitCompleted; - var _ = Task.Run(async () => await _loader.InitializeAsync(cancellationToken)); - - pluginInterface.UiBuilder.Draw += Draw; - pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; - pluginInterface.LanguageChanged += LanguageChanged; - _clientState.Login += Login; + // set up the current UI language before creating anything + Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage); _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) { HelpMessage = Localization.Command_pal_HelpText }); + + Task.Run(async () => await CreateDependencyContext()); } - private void InitCompleted(Action? loginAction) - { - LanguageChanged(_pluginInterface.UiLanguage); + 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(() => + { + _pluginInterface.UiBuilder.Draw += Draw; + _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; + _pluginInterface.LanguageChanged += LanguageChanged; + _clientState.Login += Login; + }); + _rootScopeCompletionSource.SetResult(_rootScope); + _loadState = ELoadState.Loaded; + } + catch (ObjectDisposedException e) + { + _rootScopeCompletionSource.SetException(e); + _loadState = ELoadState.Error; + } + catch (OperationCanceledException e) + { + _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(); @@ -89,84 +139,106 @@ namespace Pal.Client { arguments = arguments.Trim(); - IPalacePalConfiguration configuration = - _rootScope.ServiceProvider.GetRequiredService(); - if (configuration.FirstUse && arguments != "" && arguments != "config") + Task.Run(async () => { - _chat.Error(Localization.Error_FirstTimeSetupRequired); - return; - } - - try - { - var sp = _rootScope.ServiceProvider; - - switch (arguments) + IServiceScope rootScope; + try { - case "": - case "config": - sp.GetRequiredService().Execute(); - break; - - case "stats": - sp.GetRequiredService().Execute(); - break; - - case "tc": - case "test-connection": - sp.GetRequiredService().Execute(); - break; - - case "near": - case "tnear": - case "hnear": - sp.GetRequiredService().Execute(arguments); - break; - - default: - _chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, - command)); - break; + rootScope = await _rootScopeCompletionSource.Task; } - } - catch (Exception e) - { - _chat.Error(e.ToString()); - } + catch (Exception e) + { + _logger.LogError(e, "Could not wait for command root scope"); + return; + } + + IPalacePalConfiguration configuration = + rootScope.ServiceProvider.GetRequiredService(); + Chat chat = rootScope.ServiceProvider.GetRequiredService(); + if (configuration.FirstUse && arguments != "" && arguments != "config") + { + chat.Error(Localization.Error_FirstTimeSetupRequired); + return; + } + + try + { + var sp = rootScope.ServiceProvider; + + switch (arguments) + { + case "": + case "config": + sp.GetRequiredService().Execute(); + break; + + case "stats": + sp.GetRequiredService().Execute(); + break; + + case "tc": + case "test-connection": + sp.GetRequiredService().Execute(); + break; + + case "near": + case "tnear": + case "hnear": + sp.GetRequiredService().Execute(arguments); + break; + + default: + chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, + command)); + break; + } + } + catch (Exception e) + { + chat.Error(e.ToString()); + } + }); } private void OpenConfigUi() - => _rootScope.ServiceProvider.GetRequiredService().Execute(); + => _rootScope!.ServiceProvider.GetRequiredService().Execute(); private void LanguageChanged(string languageCode) { _logger.LogInformation("Language set to '{Language}'", languageCode); Localization.Culture = new CultureInfo(languageCode); - _windowSystem.Windows.OfType() + _windowSystem!.Windows.OfType() .Each(w => w.LanguageChanged()); } private void Draw() { - if (_loader.LoadState == DependencyInjectionLoader.ELoadState.Loaded) - { - _rootScope.ServiceProvider.GetRequiredService().DrawLayers(); - _windowSystem.Draw(); - } + _rootScope!.ServiceProvider.GetRequiredService().DrawLayers(); + _windowSystem!.Draw(); } public void Dispose() { _commandManager.RemoveHandler("/pal"); - _pluginInterface.UiBuilder.Draw -= Draw; - _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; - _pluginInterface.LanguageChanged -= LanguageChanged; - _clientState.Login -= Login; + if (_loadState == ELoadState.Loaded) + { + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; + _pluginInterface.LanguageChanged -= LanguageChanged; + _clientState.Login -= Login; + } - _loader.InitCompleted -= InitCompleted; - _rootScope.Dispose(); + _initCts.Cancel(); + _rootScope?.Dispose(); + } + + private enum ELoadState + { + Initializing, + Loaded, + Error } } } diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index c86ad36..5397be3 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -27,7 +27,7 @@ namespace Pal.Client.Scheduled { if (queued is T t) { - _logger.LogInformation("Handling {QueuedType}", queued.GetType()); + _logger.LogDebug("Handling {QueuedType}", queued.GetType()); Run(t, ref recreateLayout); } else From 26b3a54ebdd26b0de1622a03c34a1a2660fe3ab6 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 22 Feb 2023 23:58:05 +0100 Subject: [PATCH 35/38] DI: Update namespaces --- Pal.Client/DependencyContextInitializer.cs | 3 ++- Pal.Client/DependencyInjection/ChatService.cs | 1 + .../GameHooks.cs} | 18 +++++++++--------- Pal.Client/DependencyInjectionContext.cs | 2 +- .../FrameworkService.cs | 8 ++------ .../TerritoryState.cs | 3 +-- 6 files changed, 16 insertions(+), 19 deletions(-) rename Pal.Client/{Hooks.cs => DependencyInjection/GameHooks.cs} (92%) rename Pal.Client/{DependencyInjection => Floors}/FrameworkService.cs (99%) rename Pal.Client/{DependencyInjection => Floors}/TerritoryState.cs (93%) diff --git a/Pal.Client/DependencyContextInitializer.cs b/Pal.Client/DependencyContextInitializer.cs index db1c679..4f29b38 100644 --- a/Pal.Client/DependencyContextInitializer.cs +++ b/Pal.Client/DependencyContextInitializer.cs @@ -16,6 +16,7 @@ using Pal.Client.Configuration; using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; using Pal.Client.Windows; namespace Pal.Client @@ -61,7 +62,7 @@ namespace Pal.Client _serviceProvider.GetRequiredService(); // initialize components that are mostly self-contained/self-registered - _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); diff --git a/Pal.Client/DependencyInjection/ChatService.cs b/Pal.Client/DependencyInjection/ChatService.cs index 4b40b82..97ffaed 100644 --- a/Pal.Client/DependencyInjection/ChatService.cs +++ b/Pal.Client/DependencyInjection/ChatService.cs @@ -6,6 +6,7 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Lumina.Excel.GeneratedSheets; using Pal.Client.Configuration; +using Pal.Client.Floors; namespace Pal.Client.DependencyInjection { diff --git a/Pal.Client/Hooks.cs b/Pal.Client/DependencyInjection/GameHooks.cs similarity index 92% rename from Pal.Client/Hooks.cs rename to Pal.Client/DependencyInjection/GameHooks.cs index cf4b373..7e7d868 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/DependencyInjection/GameHooks.cs @@ -1,18 +1,18 @@ -using Dalamud.Game.ClientState.Objects.Types; +using System; +using System.Text; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Hooking; using Dalamud.Memory; using Dalamud.Utility.Signatures; -using System; -using System.Text; -using Dalamud.Game.ClientState.Objects; using Microsoft.Extensions.Logging; -using Pal.Client.DependencyInjection; +using Pal.Client.Floors; -namespace Pal.Client +namespace Pal.Client.DependencyInjection { - internal sealed unsafe class Hooks : IDisposable + internal sealed unsafe class GameHooks : IDisposable { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ObjectTable _objectTable; private readonly TerritoryState _territoryState; private readonly FrameworkService _frameworkService; @@ -24,7 +24,7 @@ namespace Pal.Client private Hook ActorVfxCreateHook { get; init; } = null!; #pragma warning restore CS0649 - public Hooks(ILogger logger, ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) + public GameHooks(ILogger logger, ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) { _logger = logger; _objectTable = objectTable; diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 00c36cb..941db6c 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -105,7 +105,7 @@ namespace Pal.Client // plugin-specific _serviceCollection.AddScoped(); _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); _serviceCollection.AddScoped(); _serviceCollection.AddScoped(); _serviceCollection.AddScoped(sp => diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/Floors/FrameworkService.cs similarity index 99% rename from Pal.Client/DependencyInjection/FrameworkService.cs rename to Pal.Client/Floors/FrameworkService.cs index 781bf28..84fde6f 100644 --- a/Pal.Client/DependencyInjection/FrameworkService.cs +++ b/Pal.Client/Floors/FrameworkService.cs @@ -2,27 +2,23 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Numerics; using System.Runtime.InteropServices; using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Logging; -using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.Database; -using Pal.Client.Extensions; -using Pal.Client.Floors; +using Pal.Client.DependencyInjection; using Pal.Client.Net; using Pal.Client.Rendering; using Pal.Client.Scheduled; using Pal.Common; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.Floors { internal sealed class FrameworkService : IDisposable { diff --git a/Pal.Client/DependencyInjection/TerritoryState.cs b/Pal.Client/Floors/TerritoryState.cs similarity index 93% rename from Pal.Client/DependencyInjection/TerritoryState.cs rename to Pal.Client/Floors/TerritoryState.cs index 43852b8..febff4c 100644 --- a/Pal.Client/DependencyInjection/TerritoryState.cs +++ b/Pal.Client/Floors/TerritoryState.cs @@ -1,9 +1,8 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; -using Pal.Client.Scheduled; using Pal.Common; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.Floors { public sealed class TerritoryState { From 0d8f655936ba5f63b3331d3a760ceef1ea0eef45 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 23 Feb 2023 00:02:31 +0100 Subject: [PATCH 36/38] Reset remote url --- Pal.Client/Net/RemoteApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 7f94781..382f103 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -13,7 +13,7 @@ namespace Pal.Client.Net #if DEBUG public const string RemoteUrl = "http://localhost:5415"; #else - public const string RemoteUrl = "http://localhost:5415"; + 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)}"; From 8a27eca8b38f02b09b90589b6f4250839f0f543b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 23 Feb 2023 00:09:49 +0100 Subject: [PATCH 37/38] DI: Just use file.copy for backups --- Pal.Client/DependencyContextInitializer.cs | 28 +++++++++++++++------- Pal.Client/DependencyInjectionContext.cs | 3 ++- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Pal.Client/DependencyContextInitializer.cs b/Pal.Client/DependencyContextInitializer.cs index 4f29b38..0723736 100644 --- a/Pal.Client/DependencyContextInitializer.cs +++ b/Pal.Client/DependencyContextInitializer.cs @@ -30,7 +30,8 @@ namespace Pal.Client private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; - public DependencyContextInitializer(ILogger logger, IServiceProvider serviceProvider) + public DependencyContextInitializer(ILogger logger, + IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; @@ -135,16 +136,25 @@ namespace Pal.Client var pluginInterface = scope.ServiceProvider.GetRequiredService(); string backupPath = Path.Join(pluginInterface.GetPluginConfigDirectory(), $"backup-{DateTime.Today.ToUniversalTime():yyyy-MM-dd}.data.sqlite3"); - if (!File.Exists(backupPath)) + string sourcePath = Path.Join(pluginInterface.GetPluginConfigDirectory(), + DependencyInjectionContext.DatabaseFileName); + if (File.Exists(sourcePath) && !File.Exists(backupPath)) { - _logger.LogInformation("Creating database backup '{Path}'", backupPath); + if (File.Exists(sourcePath + "-shm") || File.Exists(sourcePath + "-wal")) + { + _logger.LogWarning("Could not create backup, database is open in another program"); + return; + } - 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); + _logger.LogInformation("Creating database backup '{Path}'", backupPath); + try + { + File.Copy(sourcePath, backupPath); + } + catch (Exception e) + { + _logger.LogError(e, "Could not create backup"); + } } else _logger.LogInformation("Database backup in '{Path}' already exists", backupPath); diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 941db6c..3a75509 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -33,6 +33,7 @@ namespace Pal.Client /// internal sealed class DependencyInjectionContext : IDisposable { + public const string DatabaseFileName = "palace-pal.data.sqlite3"; public static DalamudLoggerProvider LoggerProvider { get; } = new(); /// @@ -89,7 +90,7 @@ namespace Pal.Client _serviceCollection.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); _sqliteConnectionString = - $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; + $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), DatabaseFileName)}"; } public IServiceProvider BuildServiceContainer() From efeb30331ca92b953a653a771ef0228febfff1ba Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 23 Feb 2023 00:38:52 +0100 Subject: [PATCH 38/38] Db: Use precompiled model --- .../Compiled/ClientLocationEntityType.cs | 123 ++++++++++++++++++ .../ClientLocationImportHistoryEntityType.cs | 83 ++++++++++++ .../Compiled/ImportHistoryEntityType.cs | 94 +++++++++++++ .../Compiled/PalClientContextModel.cs | 28 ++++ .../Compiled/PalClientContextModelBuilder.cs | 35 +++++ .../Compiled/RemoteEncounterEntityType.cs | 92 +++++++++++++ Pal.Client/DependencyInjectionContext.cs | 4 +- Pal.Client/README.md | 5 + 8 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 Pal.Client/Database/Compiled/ClientLocationEntityType.cs create mode 100644 Pal.Client/Database/Compiled/ClientLocationImportHistoryEntityType.cs create mode 100644 Pal.Client/Database/Compiled/ImportHistoryEntityType.cs create mode 100644 Pal.Client/Database/Compiled/PalClientContextModel.cs create mode 100644 Pal.Client/Database/Compiled/PalClientContextModelBuilder.cs create mode 100644 Pal.Client/Database/Compiled/RemoteEncounterEntityType.cs diff --git a/Pal.Client/Database/Compiled/ClientLocationEntityType.cs b/Pal.Client/Database/Compiled/ClientLocationEntityType.cs new file mode 100644 index 0000000..809cbc5 --- /dev/null +++ b/Pal.Client/Database/Compiled/ClientLocationEntityType.cs @@ -0,0 +1,123 @@ +// +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace Pal.Client.Database.Compiled +{ + internal partial class ClientLocationEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + "Pal.Client.Database.ClientLocation", + typeof(ClientLocation), + baseEntityType); + + var localId = runtimeEntityType.AddProperty( + "LocalId", + typeof(int), + propertyInfo: typeof(ClientLocation).GetProperty("LocalId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + valueGenerated: ValueGenerated.OnAdd, + afterSaveBehavior: PropertySaveBehavior.Throw); + + var seen = runtimeEntityType.AddProperty( + "Seen", + typeof(bool), + propertyInfo: typeof(ClientLocation).GetProperty("Seen", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var sinceVersion = runtimeEntityType.AddProperty( + "SinceVersion", + typeof(string), + propertyInfo: typeof(ClientLocation).GetProperty("SinceVersion", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var source = runtimeEntityType.AddProperty( + "Source", + typeof(ClientLocation.ESource), + propertyInfo: typeof(ClientLocation).GetProperty("Source", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var territoryType = runtimeEntityType.AddProperty( + "TerritoryType", + typeof(ushort), + propertyInfo: typeof(ClientLocation).GetProperty("TerritoryType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var type = runtimeEntityType.AddProperty( + "Type", + typeof(ClientLocation.EType), + propertyInfo: typeof(ClientLocation).GetProperty("Type", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var x = runtimeEntityType.AddProperty( + "X", + typeof(float), + propertyInfo: typeof(ClientLocation).GetProperty("X", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var y = runtimeEntityType.AddProperty( + "Y", + typeof(float), + propertyInfo: typeof(ClientLocation).GetProperty("Y", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var z = runtimeEntityType.AddProperty( + "Z", + typeof(float), + propertyInfo: typeof(ClientLocation).GetProperty("Z", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var key = runtimeEntityType.AddKey( + new[] { localId }); + runtimeEntityType.SetPrimaryKey(key); + + return runtimeEntityType; + } + + public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType declaringEntityType, RuntimeEntityType targetEntityType, RuntimeEntityType joinEntityType) + { + var skipNavigation = declaringEntityType.AddSkipNavigation( + "ImportedBy", + targetEntityType, + joinEntityType.FindForeignKey( + new[] { joinEntityType.FindProperty("ImportedLocationsLocalId")! }, + declaringEntityType.FindKey(new[] { declaringEntityType.FindProperty("LocalId")! })!, + declaringEntityType)!, + true, + false, + typeof(List), + propertyInfo: typeof(ClientLocation).GetProperty("ImportedBy", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var inverse = targetEntityType.FindSkipNavigation("ImportedLocations"); + if (inverse != null) + { + skipNavigation.Inverse = inverse; + inverse.Inverse = skipNavigation; + } + + return skipNavigation; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation("Relational:FunctionName", null); + runtimeEntityType.AddAnnotation("Relational:Schema", null); + runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); + runtimeEntityType.AddAnnotation("Relational:TableName", "Locations"); + runtimeEntityType.AddAnnotation("Relational:ViewName", null); + runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} diff --git a/Pal.Client/Database/Compiled/ClientLocationImportHistoryEntityType.cs b/Pal.Client/Database/Compiled/ClientLocationImportHistoryEntityType.cs new file mode 100644 index 0000000..2b15bab --- /dev/null +++ b/Pal.Client/Database/Compiled/ClientLocationImportHistoryEntityType.cs @@ -0,0 +1,83 @@ +// +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace Pal.Client.Database.Compiled +{ + internal partial class ClientLocationImportHistoryEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + "ClientLocationImportHistory", + typeof(Dictionary), + baseEntityType, + sharedClrType: true, + indexerPropertyInfo: RuntimeEntityType.FindIndexerProperty(typeof(Dictionary)), + propertyBag: true); + + var importedById = runtimeEntityType.AddProperty( + "ImportedById", + typeof(Guid), + propertyInfo: runtimeEntityType.FindIndexerPropertyInfo(), + afterSaveBehavior: PropertySaveBehavior.Throw); + + var importedLocationsLocalId = runtimeEntityType.AddProperty( + "ImportedLocationsLocalId", + typeof(int), + propertyInfo: runtimeEntityType.FindIndexerPropertyInfo(), + afterSaveBehavior: PropertySaveBehavior.Throw); + + var key = runtimeEntityType.AddKey( + new[] { importedById, importedLocationsLocalId }); + runtimeEntityType.SetPrimaryKey(key); + + var index = runtimeEntityType.AddIndex( + new[] { importedLocationsLocalId }); + + return runtimeEntityType; + } + + public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("ImportedById")! }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty("Id")! })!, + principalEntityType, + deleteBehavior: DeleteBehavior.Cascade, + required: true); + + return runtimeForeignKey; + } + + public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("ImportedLocationsLocalId")! }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty("LocalId")! })!, + principalEntityType, + deleteBehavior: DeleteBehavior.Cascade, + required: true); + + return runtimeForeignKey; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation("Relational:FunctionName", null); + runtimeEntityType.AddAnnotation("Relational:Schema", null); + runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); + runtimeEntityType.AddAnnotation("Relational:TableName", "LocationImports"); + runtimeEntityType.AddAnnotation("Relational:ViewName", null); + runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} diff --git a/Pal.Client/Database/Compiled/ImportHistoryEntityType.cs b/Pal.Client/Database/Compiled/ImportHistoryEntityType.cs new file mode 100644 index 0000000..c3f25d8 --- /dev/null +++ b/Pal.Client/Database/Compiled/ImportHistoryEntityType.cs @@ -0,0 +1,94 @@ +// +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace Pal.Client.Database.Compiled +{ + internal partial class ImportHistoryEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + "Pal.Client.Database.ImportHistory", + typeof(ImportHistory), + baseEntityType); + + var id = runtimeEntityType.AddProperty( + "Id", + typeof(Guid), + propertyInfo: typeof(ImportHistory).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ImportHistory).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + valueGenerated: ValueGenerated.OnAdd, + afterSaveBehavior: PropertySaveBehavior.Throw); + + var exportedAt = runtimeEntityType.AddProperty( + "ExportedAt", + typeof(DateTime), + propertyInfo: typeof(ImportHistory).GetProperty("ExportedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ImportHistory).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var importedAt = runtimeEntityType.AddProperty( + "ImportedAt", + typeof(DateTime), + propertyInfo: typeof(ImportHistory).GetProperty("ImportedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ImportHistory).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var remoteUrl = runtimeEntityType.AddProperty( + "RemoteUrl", + typeof(string), + propertyInfo: typeof(ImportHistory).GetProperty("RemoteUrl", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ImportHistory).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + + var key = runtimeEntityType.AddKey( + new[] { id }); + runtimeEntityType.SetPrimaryKey(key); + + return runtimeEntityType; + } + + public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType declaringEntityType, RuntimeEntityType targetEntityType, RuntimeEntityType joinEntityType) + { + var skipNavigation = declaringEntityType.AddSkipNavigation( + "ImportedLocations", + targetEntityType, + joinEntityType.FindForeignKey( + new[] { joinEntityType.FindProperty("ImportedById")! }, + declaringEntityType.FindKey(new[] { declaringEntityType.FindProperty("Id")! })!, + declaringEntityType)!, + true, + false, + typeof(List), + propertyInfo: typeof(ImportHistory).GetProperty("ImportedLocations", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ImportHistory).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var inverse = targetEntityType.FindSkipNavigation("ImportedBy"); + if (inverse != null) + { + skipNavigation.Inverse = inverse; + inverse.Inverse = skipNavigation; + } + + return skipNavigation; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation("Relational:FunctionName", null); + runtimeEntityType.AddAnnotation("Relational:Schema", null); + runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); + runtimeEntityType.AddAnnotation("Relational:TableName", "Imports"); + runtimeEntityType.AddAnnotation("Relational:ViewName", null); + runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} diff --git a/Pal.Client/Database/Compiled/PalClientContextModel.cs b/Pal.Client/Database/Compiled/PalClientContextModel.cs new file mode 100644 index 0000000..61cf3d0 --- /dev/null +++ b/Pal.Client/Database/Compiled/PalClientContextModel.cs @@ -0,0 +1,28 @@ +// +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace Pal.Client.Database.Compiled +{ + [DbContext(typeof(PalClientContext))] + public partial class PalClientContextModel : RuntimeModel + { + static PalClientContextModel() + { + var model = new PalClientContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; + } + + private static PalClientContextModel _instance; + public static IModel Instance => _instance; + + partial void Initialize(); + + partial void Customize(); + } +} diff --git a/Pal.Client/Database/Compiled/PalClientContextModelBuilder.cs b/Pal.Client/Database/Compiled/PalClientContextModelBuilder.cs new file mode 100644 index 0000000..81f2a22 --- /dev/null +++ b/Pal.Client/Database/Compiled/PalClientContextModelBuilder.cs @@ -0,0 +1,35 @@ +// +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace Pal.Client.Database.Compiled +{ + public partial class PalClientContextModel + { + partial void Initialize() + { + var clientLocationImportHistory = ClientLocationImportHistoryEntityType.Create(this); + var clientLocation = ClientLocationEntityType.Create(this); + var importHistory = ImportHistoryEntityType.Create(this); + var remoteEncounter = RemoteEncounterEntityType.Create(this); + + ClientLocationImportHistoryEntityType.CreateForeignKey1(clientLocationImportHistory, importHistory); + ClientLocationImportHistoryEntityType.CreateForeignKey2(clientLocationImportHistory, clientLocation); + RemoteEncounterEntityType.CreateForeignKey1(remoteEncounter, clientLocation); + + ClientLocationEntityType.CreateSkipNavigation1(clientLocation, importHistory, clientLocationImportHistory); + ImportHistoryEntityType.CreateSkipNavigation1(importHistory, clientLocation, clientLocationImportHistory); + + ClientLocationImportHistoryEntityType.CreateAnnotations(clientLocationImportHistory); + ClientLocationEntityType.CreateAnnotations(clientLocation); + ImportHistoryEntityType.CreateAnnotations(importHistory); + RemoteEncounterEntityType.CreateAnnotations(remoteEncounter); + + AddAnnotation("ProductVersion", "7.0.3"); + } + } +} diff --git a/Pal.Client/Database/Compiled/RemoteEncounterEntityType.cs b/Pal.Client/Database/Compiled/RemoteEncounterEntityType.cs new file mode 100644 index 0000000..a2df98b --- /dev/null +++ b/Pal.Client/Database/Compiled/RemoteEncounterEntityType.cs @@ -0,0 +1,92 @@ +// +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace Pal.Client.Database.Compiled +{ + internal partial class RemoteEncounterEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + "Pal.Client.Database.RemoteEncounter", + typeof(RemoteEncounter), + baseEntityType); + + var id = runtimeEntityType.AddProperty( + "Id", + typeof(int), + propertyInfo: typeof(RemoteEncounter).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(RemoteEncounter).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + valueGenerated: ValueGenerated.OnAdd, + afterSaveBehavior: PropertySaveBehavior.Throw); + + var accountId = runtimeEntityType.AddProperty( + "AccountId", + typeof(string), + propertyInfo: typeof(RemoteEncounter).GetProperty("AccountId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(RemoteEncounter).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + maxLength: 13); + + var clientLocationId = runtimeEntityType.AddProperty( + "ClientLocationId", + typeof(int), + propertyInfo: typeof(RemoteEncounter).GetProperty("ClientLocationId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(RemoteEncounter).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var key = runtimeEntityType.AddKey( + new[] { id }); + runtimeEntityType.SetPrimaryKey(key); + + var index = runtimeEntityType.AddIndex( + new[] { clientLocationId }); + + return runtimeEntityType; + } + + public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("ClientLocationId")! }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty("LocalId")! })!, + principalEntityType, + deleteBehavior: DeleteBehavior.Cascade, + required: true); + + var clientLocation = declaringEntityType.AddNavigation("ClientLocation", + runtimeForeignKey, + onDependent: true, + typeof(ClientLocation), + propertyInfo: typeof(RemoteEncounter).GetProperty("ClientLocation", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(RemoteEncounter).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var remoteEncounters = principalEntityType.AddNavigation("RemoteEncounters", + runtimeForeignKey, + onDependent: false, + typeof(List), + propertyInfo: typeof(ClientLocation).GetProperty("RemoteEncounters", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(ClientLocation).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + return runtimeForeignKey; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation("Relational:FunctionName", null); + runtimeEntityType.AddAnnotation("Relational:Schema", null); + runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); + runtimeEntityType.AddAnnotation("Relational:TableName", "RemoteEncounters"); + runtimeEntityType.AddAnnotation("Relational:ViewName", null); + runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 3a75509..cc967cc 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -99,7 +99,9 @@ namespace Pal.Client typeof(DependencyInjectionContext).Assembly.FullName); // EF core - _serviceCollection.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); + _serviceCollection.AddDbContext(o => o + .UseSqlite(_sqliteConnectionString) + .UseModel(Database.Compiled.PalClientContextModel.Instance)); _serviceCollection.AddTransient(); _serviceCollection.AddScoped(); diff --git a/Pal.Client/README.md b/Pal.Client/README.md index 52bd208..71ef729 100644 --- a/Pal.Client/README.md +++ b/Pal.Client/README.md @@ -13,3 +13,8 @@ To use with `dotnet ef` commands, specify it as `-c EF`, for example: ```shell dotnet ef migrations add MigrationName --configuration EF ``` + +To rebuild the compiled model: +```shell +dotnet ef dbcontext optimize --output-dir Database/Compiled --namespace Pal.Client.Database.Compiled --configuration EF +```