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 + }; + } + } } }