From a1f2b22b193dfaf289ab04d3d457bd030bd7f4d2 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 21:59:29 +0200 Subject: [PATCH] Drop schema migrations and move AutoTranslate.PreloadCache off the load path Migrations: all current users are on schema v16, the v9 to v16 migration chain ran in v1.2.1 and earlier. Replace the seven in-LoadAsync migration blocks with a hard schema-gate in the Phase-1 ctor; older configs trigger a clear "install v1.4.2 first" error. Code-hygiene change, fast-path saving is negligible. Remove the now-unused TryReadPreV13ThemeOpacity helper that only served the v13 to v14 block. AutoTranslate.PreloadCache: was sync ~300 ms in LoadAsync. Move to Task.Run so plugin-load returns ~300 ms earlier. Trade-off: first auto-translate use of a session may have a sub-second hitch if the cache hasn't finished warming. Acceptable, it is first-use cost instead of every-load cost. --- HellionChat/Plugin.cs | 357 ++---------------------------------------- 1 file changed, 17 insertions(+), 340 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 41c63a1..54df6f4 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -129,6 +129,17 @@ public sealed class Plugin : IAsyncDalamudPlugin Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); + // Schema-gate: v1.4.3 only supports config schema v16. Older configs + // went through their migrations in v1.2.1 (v15→v16) and earlier; users + // who skipped past those releases need to install v1.4.2 first to run + // the migration chain, then upgrade to v1.4.3. + if (Config.Version < 16) + { + throw new InvalidOperationException( + $"HellionChat v1.4.3 requires config schema v16, got v{Config.Version}. " + + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.3."); + } + // Hellion Chat — Auto-Tell-Tabs Defense-in-Depth. SaveConfig // already strips temp tabs before persistence, but a previous // crash or external write could have left them in the JSON. @@ -150,308 +161,6 @@ public sealed class Plugin : IAsyncDalamudPlugin try { - // Schema migrations live in LoadAsync so the sync ctor returns fast. - // Each block is self-contained and runs sequentially before any - // service consumes Config; order matters because later versions - // build on earlier ones (e.g. v16 reads opacity that v14 wrote). - - // Hellion Chat v9 → v10 — wipes the configuration so the new 8-tab - // layout starts from defaults instead of mapping every previous setting - // to its new position. Backup-Failure ist non-fatal, der Wipe läuft - // trotzdem; dem User fehlt dann nur das manuelle Restore-Sicherheitsnetz. - if (Config.Version < 10) - { - var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; - if (pluginConfigsDir is not null) - { - var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json"); - var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v10-backup"); - - try - { - if (File.Exists(liveConfigPath)) - { - File.Copy(liveConfigPath, backupPath, overwrite: true); - } - } - catch (Exception ex) - { - Log.Warning(ex, "HellionChat: pre-v10 config backup failed"); - } - } - - Config = new Configuration - { - Version = 10, - FirstRunCompleted = true, - }; - SaveConfig(); - - Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification - { - Title = HellionStrings.SettingsRefactor_Migration_Title, - Content = HellionStrings.SettingsRefactor_Migration_Content, - Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, - InitialDuration = TimeSpan.FromSeconds(25), - }); - } - - // Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled - // master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out - // input feature. Lightweight migration: defaults both fields, - // no user-facing notification because the change is opt-in only. - if (Config.Version < 11) - { - Config.PopOutInputEnabled = false; - Config.SeenPopOutInputHint = false; - Config.Version = 11; - SaveConfig(); - Log.Information( - "Migrated config v10 → v11: PopOutInputEnabled added (global, default off), " + - "SeenPopOutInputHint added (default false)"); - } - - // Hellion Chat v11 → v12 — flips Configuration.PopOutInputEnabled from - // the v0.6.0 opt-in default (false) to opt-out (true) per v0.6.1 UX - // polish. Hard-flip is a deliberate design call (see Spec section 5.7); - // users are notified via the v0.6.1 hint banner (SeenPopOutHeaderHint - // reset). Re-toggle after migration is preserved because this block - // only fires for Version < 12. - if (Config.Version < 12) - { - Config.PopOutInputEnabled = true; - Config.SeenPopOutHeaderHint = false; - Config.Version = 12; - SaveConfig(); - Log.Information( - "Migrated config v11 → v12: PopOutInputEnabled hard-flipped to true (v0.6.1 default), " + - "SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)"); - } - - // Hellion Chat v12 → v13 — hard-resets the tab layout to the - // sharpened v1.0.0 defaults (5 thematic tabs, see TabsUtil and - // the default-fill block below). Existing tab state is wiped - // because per-channel mapping from the old General preset to - // the new General/System split would be ambiguous and would - // produce subtly wrong results for users who tweaked the old - // layout. A timestamped backup of the live config is written - // alongside it as a manual restore safety net. The wipe scope - // is intentionally narrow: only Config.Tabs is reset; Privacy, - // Retention, Theme and every other knob keeps its current value. - if (Config.Version < 13) - { - var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; - if (pluginConfigsDir is not null) - { - var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json"); - var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup"); - - try - { - if (File.Exists(liveConfigPath)) - File.Copy(liveConfigPath, backupPath, overwrite: true); - } - catch (Exception ex) - { - Log.Warning(ex, "HellionChat: pre-v13 config backup failed"); - } - } - - Config.Tabs.Clear(); - Config.Version = 13; - SaveConfig(); - - Log.Information( - "Migrated config v12 → v13: tab layout hard-reset to v1.0.0 defaults; " + - "pre-v13 config backup written next to the live file. " + - "Default tabs will be populated by the Tabs.Count == 0 block."); - - Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification - { - Title = HellionStrings.SettingsRefactor_Migration_Title, - Content = HellionStrings.SettingsRefactor_Migration_Content, - Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, - InitialDuration = TimeSpan.FromSeconds(25), - }); - } - - // Hellion Chat v13 → v14 — theme-engine migration. Alle User landen - // auf "hellion-arctic" als neues Default-Theme; die alte - // HellionThemeEnabled-Flag wird deprecated und nur noch ein Release - // als Safety-Net im JSON behalten. Window-Opacity wandert von - // HellionThemeWindowOpacity in das neue WindowOpacity-Feld. - // - // v1.4.0 (F5.4): Pre-v13-Backup wird gelesen, HellionThemeWindowOpacity - // ins neue Feld gezogen. Override nur wenn WindowOpacity noch beim - // Default sitzt — sonst hat der User in der Zwischenzeit (z.B. via - // WindowAlpha → WindowOpacity in v15→v16) explizit etwas gesetzt. - if (Config.Version < 14) - { - Config.Theme = "hellion-arctic"; - - var oldThemeOpacity = TryReadPreV13ThemeOpacity(); - if (oldThemeOpacity is { } legacy - && Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f) - { - Config.WindowOpacity = Math.Clamp(legacy, 0.5f, 1.0f); - Log.Information( - $"Migrated pre-v13 HellionThemeWindowOpacity {legacy} to WindowOpacity {Config.WindowOpacity}"); - } - - Config.ReduceMotion = false; - Config.UseCompactDensity = false; - Config.Version = 14; - SaveConfig(); - Log.Information( - "Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " + - "pick chat2-classic in Settings → Themes for the upstream look"); - } - - if (Config.Version < 15) - { - // v1.2.0 — keine Datenmigration nötig. Removal der deprecated - // Theme-Felder ist reine Schema-Bereinigung (System.Text.Json - // ignoriert unbekannte Felder im JSON, daher kein Crash bei - // Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity - // serialisiert haben — die Werte verfallen einfach). - Config.Version = 15; - SaveConfig(); - Log.Information( - "Migrated config v14 → v15: legacy theme fields removed " + - "(HellionThemeEnabled, HellionThemeWindowOpacity)"); - } - - // Hellion Chat v15 → v16 — Settings Cleanup. Re-Sortierung der - // Tabs auf der UI-Seite (datenneutral). 4 tote Felder verfallen - // beim System.Text.Json-Deserialize (OverrideStyle, ChosenStyle, - // WindowAlpha, ShowThemeQuickPicker — sind alle nicht mehr im - // Configuration-Schema definiert). WindowAlpha wird zuvor auf - // WindowOpacity gemappt damit User die ihn gesetzt hatten ihre - // Transparenz-Einstellung behalten. - if (Config.Version < 16) - { - var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; - var liveConfigPath = pluginConfigsDir is not null - ? Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json") - : null; - - // Backup-Datei neben der live Config — Pattern aus v13 Branch. - if (pluginConfigsDir is not null && liveConfigPath is not null) - { - var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v16-backup"); - try - { - if (File.Exists(liveConfigPath)) - File.Copy(liveConfigPath, backupPath, overwrite: true); - } - catch (Exception ex) - { - Log.Warning(ex, "HellionChat: pre-v16 config backup failed"); - } - } - - // Pre-v16 Felder einmalig roh aus dem JSON lesen, da sie nicht - // mehr im Configuration-Schema sind (und damit aus Config nicht - // mehr abrufbar). WindowAlpha → WindowOpacity Mapping nur wenn - // User WindowOpacity noch nicht selbst angefasst hat (Default - // 0.85), sonst gewinnt der User-Wert. - float oldWindowAlpha = 100f; - bool oldOverrideStyle = false; - if (liveConfigPath is not null) - { - try - { - if (File.Exists(liveConfigPath)) - { - var rawJson = File.ReadAllText(liveConfigPath); - using var doc = System.Text.Json.JsonDocument.Parse(rawJson); - if (doc.RootElement.TryGetProperty("WindowAlpha", out var alphaProp) - && alphaProp.ValueKind == System.Text.Json.JsonValueKind.Number) - { - oldWindowAlpha = alphaProp.GetSingle(); - } - if (doc.RootElement.TryGetProperty("OverrideStyle", out var ovProp) - && ovProp.ValueKind is System.Text.Json.JsonValueKind.True) - { - oldOverrideStyle = true; - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "HellionChat: pre-v16 legacy-field lookup failed, defaults assumed"); - } - } - - // TEST-MIRROR: Hellion Build test/_Helpers/MigrationLogic.cs (local-only repo) - // If you change the formula here, change the mirror — tests assert the contract. - if (oldWindowAlpha != 100f - && Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f) - { - Config.WindowOpacity = Math.Clamp(oldWindowAlpha / 100f, 0.5f, 1.0f); - Log.Information( - $"Migrated WindowAlpha {oldWindowAlpha} to WindowOpacity {Config.WindowOpacity}"); - } - else if (oldWindowAlpha != 100f) - { - Log.Information( - $"Skipped WindowAlpha→WindowOpacity migration: WindowOpacity already user-set " + - $"({Config.WindowOpacity}), legacy WindowAlpha value {oldWindowAlpha} dropped."); - } - - if (oldOverrideStyle) - { - Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification - { - Title = "Hellion Chat 1.2.1", - Content = HellionStrings.Migration_v16_OverrideStyle_Toast, - Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, - InitialDuration = TimeSpan.FromSeconds(25), - }); - } - - // v1.2.1 Default-Bumps für UX-Verbesserungen. Pattern: nur - // migrieren wenn der User noch auf dem alten Default ist. - // Bei bool-Werten ist die Erkennung pragmatisch — wer den - // alten Default aktiv ausgeschaltet hatte, erlebt das als - // Regression und stellt es einmal in den Settings zurück. - // Der Trade-Off ist akzeptabel weil die alten Defaults in - // v1.2.0 erst neu eingeführt wurden und kaum jemand aktiv - // umgeschaltet hat. - if (!Config.UseCompactDensity) - { - Config.UseCompactDensity = true; - Log.Information("v16 default-bump: UseCompactDensity false → true"); - } - if (!Config.HideInNewGamePlusMenu) - { - Config.HideInNewGamePlusMenu = true; - Log.Information("v16 default-bump: HideInNewGamePlusMenu false → true"); - } - if (!Config.HideSameTimestamps) - { - Config.HideSameTimestamps = true; - Log.Information("v16 default-bump: HideSameTimestamps false → true"); - } - if (Config.MaxLinesToRender == 5000) - { - Config.MaxLinesToRender = 2500; - Log.Information("v16 default-bump: MaxLinesToRender 5000 → 2500"); - } - if (Config.ChatColours.Count == 0) - { - foreach (var (channel, colour) in Resources.ChatColourPresets.All["Hellion"].Colours) - Config.ChatColours[channel] = colour; - Log.Information("v16 default-bump: ChatColours empty → Hellion brand preset"); - } - - Config.Version = 16; - SaveConfig(); - Log.Information( - "Migrated config v15 → v16: settings cleanup, " + - "OverrideStyle/ChosenStyle/WindowAlpha/ShowThemeQuickPicker dropped from schema"); - } - // Hellion v1.0.0 default tab layout. Five thematically separated // tabs: General catches the immediate-surroundings public chat // (Say/Yell/Shout) only; System absorbs the rest of the technical @@ -560,10 +269,12 @@ public sealed class Plugin : IAsyncDalamudPlugin Interface.UiBuilder.DisableGposeUiHide = true; #if !DEBUG - // Avoid 300ms hitch when sending first message by preloading the - // auto-translate cache. Don't do this in debug because it makes - // profiling difficult. - AutoTranslate.PreloadCache(); + // Fire-and-forget on a worker thread. The first auto-translate use of + // a session may have a sub-second hitch if the cache hasn't filled yet, + // but that's preferable to making every user wait ~300 ms during + // plugin load for a cache they may never touch. ChatTwo (upstream) + // does this sync; we trade load-time for first-use latency. + _ = Task.Run(AutoTranslate.PreloadCache, cancellationToken); #endif cancellationToken.ThrowIfCancellationRequested(); @@ -702,40 +413,6 @@ public sealed class Plugin : IAsyncDalamudPlugin return failure; } - // Reads HellionThemeWindowOpacity from the pre-v13 backup the v12→v13 - // block writes alongside the live config. Null when absent, unreadable, - // or schema-incompatible — all valid steady states (fresh install, - // backup pruned, pre-v12 config). Errors log at Warning so a corrupted - // backup stays visible in /xllog without breaking the migration. - private static float? TryReadPreV13ThemeOpacity() - { - var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; - if (pluginConfigsDir is null) - return null; - - var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup"); - if (!File.Exists(backupPath)) - return null; - - try - { - using var stream = File.OpenRead(backupPath); - using var doc = System.Text.Json.JsonDocument.Parse(stream); - if (doc.RootElement.TryGetProperty("HellionThemeWindowOpacity", out var prop) - && prop.ValueKind == System.Text.Json.JsonValueKind.Number - && prop.TryGetSingle(out var value)) - { - return value; - } - return null; - } - catch (Exception ex) - { - Log.Warning(ex, "HellionChat: pre-v13 backup lookup failed, defaulting WindowOpacity"); - return null; - } - } - private static void MigrateFromChatTwoLayout() { var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;