using Dalamud.Configuration; 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 System.Security.Cryptography; namespace Pal.Client { public class Configuration : IPluginConfiguration { 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 public bool FirstUse { get; set; } = true; public EMode Mode { get; set; } = EMode.Offline; [Obsolete] public string? DebugAccountId { private get; set; } [Obsolete] public string? AccountId { private get; set; } [Obsolete] public Dictionary AccountIds { private get; set; } = new(); public Dictionary Accounts { get; set; } = new(); public List ImportHistory { get; set; } = new(); public bool ShowTraps { get; set; } = true; public Vector4 TrapColor { get; set; } = new Vector4(1, 0, 0, 0.4f); public bool OnlyVisibleTrapsAfterPomander { get; set; } = true; public bool ShowHoard { get; set; } = true; public Vector4 HoardColor { get; set; } = new Vector4(0, 1, 1, 0.4f); public bool OnlyVisibleHoardAfterPomander { get; set; } = true; public bool ShowSilverCoffers { get; set; } = false; public Vector4 SilverCofferColor { get; set; } = new Vector4(1, 1, 1, 0.4f); public bool FillSilverCoffers { get; set; } = true; /// /// Needs to be manually set. /// public string BetaKey { get; set; } = ""; #endregion #pragma warning disable CS0612 // Type or member is obsolete public void Migrate() { if (Version == 1) { PluginLog.Information("Updating config to version 2"); if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId)) AccountIds["http://localhost:5145"] = debugAccountId; if (AccountId != null && Guid.TryParse(AccountId, out Guid accountId)) AccountIds["https://pal.μ.tv"] = accountId; Version = 2; Save(); } if (Version == 2) { PluginLog.Information("Updating config to version 3"); Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo { Id = x.Value }); Version = 3; Save(); } if (Version == 3) { Version = 4; Save(); } if (Version == 4) { // 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up. // Not a problem for online players, but offline players might be fucked. bool changedAnyFile = false; LocalState.ForEach(s => { foreach (var marker in s.Markers) marker.SinceVersion = "0.0"; var lastModified = File.GetLastWriteTimeUtc(s.GetSaveLocation()); if (lastModified >= new DateTime(2023, 2, 3, 0, 0, 0, DateTimeKind.Utc)) { s.Backup(suffix: "bak"); s.Markers = new ConcurrentBag(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == Marker.EType.Hoard || m.WasImported)); s.Save(); changedAnyFile = true; } else { // just add version information, nothing else s.Save(); } }); // Only notify offline users - we can just re-download the backup markers from the server seamlessly. if (Mode == EMode.Offline && changedAnyFile) { new TickScheduler(delegate { Service.Chat.PrintError("[Palace Pal] Due to a bug, some coffers were accidentally saved as traps. To fix the related display issue, locally cached data was cleaned up."); Service.Chat.PrintError($"If you have any backup tools installed, please restore the contents of '{Service.PluginInterface.GetPluginConfigDirectory()}' to any backup from February 2, 2023 or before."); Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023."); }, 2500); } Version = 5; Save(); } if (Version == 5) { LocalState.UpdateAll(); Version = 6; Save(); } } #pragma warning restore CS0612 // Type or member is obsolete public void Save() { Service.PluginInterface.SavePluginConfig(this); 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 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 List CachedRoles { get; set; } = new List(); } 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; } public string? RemoteUrl { get; set; } public DateTime ExportedAt { get; set; } /// /// Set when the file is imported locally. /// public DateTime ImportedAt { get; set; } } } }