From a531973c0dc7fe3e0d78348ddadf9a2e4340d5ce Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 19:23:53 +0200 Subject: [PATCH 01/10] Refactor Plugin to IAsyncDalamudPlugin two-phase load --- HellionChat/Plugin.cs | 957 +++++++++++++++++++++++------------------- 1 file changed, 530 insertions(+), 427 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index f7a4d42..0d05778 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Runtime.ExceptionServices; using HellionChat.Ipc; using HellionChat.Resources; using HellionChat.Ui; @@ -17,7 +18,7 @@ using Dalamud.Interface.ImGuiFileDialog; namespace HellionChat; // ReSharper disable once ClassNeverInstantiated.Global -public sealed class Plugin : IDalamudPlugin +public sealed class Plugin : IAsyncDalamudPlugin { public const string PluginName = "Hellion Chat"; @@ -47,27 +48,35 @@ public sealed class Plugin : IDalamudPlugin public static FileDialogManager FileDialogManager { get; private set; } = null!; public readonly WindowSystem WindowSystem = new(PluginName); - public SettingsWindow SettingsWindow { get; } - public ChatLogWindow ChatLogWindow { get; } - public DbViewer DbViewer { get; } - public InputPreview InputPreview { get; } - public CommandHelpWindow CommandHelpWindow { get; } - public SeStringDebugger SeStringDebugger { get; } - public FirstRunWizard FirstRunWizard { get; } - public DebuggerWindow DebuggerWindow { get; } - internal Commands Commands { get; } - internal GameFunctions.GameFunctions Functions { get; } - internal MessageManager MessageManager { get; } - internal AutoTellTabsService AutoTellTabsService { get; } - internal IpcManager Ipc { get; } - internal ExtraChat ExtraChat { get; } - internal TypingIpc TypingIpc { get; } - internal FontManager FontManager { get; } + // v1.4.3: Phase-2 services need private setters now that LoadAsync + // owns their construction. Phase-1-only services (Commands, Functions, + // Ipc, ExtraChat, TypingIpc) keep their setters for symmetry. + public SettingsWindow SettingsWindow { get; private set; } = null!; + public ChatLogWindow ChatLogWindow { get; private set; } = null!; + public DbViewer DbViewer { get; private set; } = null!; + public InputPreview InputPreview { get; private set; } = null!; + public CommandHelpWindow CommandHelpWindow { get; private set; } = null!; + public SeStringDebugger SeStringDebugger { get; private set; } = null!; + public FirstRunWizard FirstRunWizard { get; private set; } = null!; + public DebuggerWindow DebuggerWindow { get; private set; } = null!; + + internal Commands Commands { get; private set; } = null!; + internal GameFunctions.GameFunctions Functions { get; private set; } = null!; + internal MessageManager MessageManager { get; private set; } = null!; + internal AutoTellTabsService AutoTellTabsService { get; private set; } = null!; + internal IpcManager Ipc { get; private set; } = null!; + internal ExtraChat ExtraChat { get; private set; } = null!; + internal TypingIpc TypingIpc { get; private set; } = null!; + internal FontManager FontManager { get; private set; } = null!; internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!; internal Ui.StatusBar StatusBar { get; private set; } = null!; internal Integrations.HonorificService HonorificService { get; private set; } = null!; + // (B3) Lightless idempotency guard — Dalamud may fire DisposeAsync twice + // in a reload race; second call short-circuits. + private int _disposeStarted; + internal int DeferredSaveFrames = -1; // Serialises retention sweeps. The 24h auto-sweep on plugin load and @@ -98,362 +107,409 @@ public sealed class Plugin : IDalamudPlugin public Plugin() { + // v1.4.3 Phase 1 — sync ctor: only work that doesn't touch + // Theme/Font/MessageManager/UI-windows lives here. Async Phase 2 + // (LoadAsync) owns those. Hooks subscribe at the END of LoadAsync + // so Dalamud's first Draw tick never sees null services (B1). + // Refuse to start if upstream Chat 2 is loaded — prevents IPC // channel collisions and double-replacement of the in-game chat // window. Throwing here makes Dalamud abort the load cleanly with // our localized message instead of crashing FFXIV mid-frame. ChatTwoConflictDetector.ThrowIfChatTwoIsLoaded(Interface); + GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime(); + + // Hellion Chat: take over config + database from upstream ChatTwo + // before Dalamud loads our plugin config. Idempotent: only acts on + // the first start where the legacy paths exist and ours don't. + MigrateFromChatTwoLayout(); + + Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); + + // 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. + // Drop them on load to guarantee the session-only invariant. + Config.Tabs.RemoveAll(t => t.IsTempTab); + + // 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 + // and gameplay-event noise; FreeCompany, Group and Linkshell each + // own their respective channel set. Tells are not in a static + // tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation + // tabs on demand. Novice-Network gets no preset tab; users who + // want it can add HellionBeginner from Settings → Tabs. + if (Config.Tabs.Count == 0) + { + Config.Tabs.Add(TabsUtil.VanillaGeneral); + Config.Tabs.Add(TabsUtil.HellionSystem); + Config.Tabs.Add(TabsUtil.HellionFreeCompany); + Config.Tabs.Add(TabsUtil.HellionParty); + Config.Tabs.Add(TabsUtil.HellionLinkshell); + } + + LanguageChanged(Interface.UiLanguage); + ImGuiUtil.Initialize(this); + + FileDialogManager = new FileDialogManager(); + + // Phase-1 services: pure allocation, no Theme/Font/UI touch. + Commands = new Commands(); + Functions = new GameFunctions.GameFunctions(this); + Ipc = new IpcManager(); + TypingIpc = new TypingIpc(this); + ExtraChat = new ExtraChat(); + + // Plugin integrations register their IPC subscribers up-front so + // Ready/Disposing events from the target plugins are caught from + // the very first frame, even if the user's Honorific reloads + // mid-session. See HellionChat/Integrations/HonorificService.cs. + HonorificService = new Integrations.HonorificService(Interface, Log, Framework); + + StatusBar = new Ui.StatusBar(); + + Interface.UiBuilder.DisableCutsceneUiHide = true; + Interface.UiBuilder.DisableGposeUiHide = true; + + // Hellion Chat — daily retention sweep, off-thread so it never + // blocks plugin load. Skips itself when disabled or already ran + // within the past 24 hours. Pure fire-and-forget on a background + // thread, so it doesn't depend on Phase-2 services being live. + RunRetentionSweepIfDue(); + + if (Config.ShowEmotes) + _ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside + } + + public async Task LoadAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try { - GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime(); - - // Hellion Chat: take over config + database from upstream ChatTwo - // before Dalamud loads our plugin config. Idempotent: only acts on - // the first start where the legacy paths exist and ours don't. - MigrateFromChatTwoLayout(); - - Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); - - // 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. - // Drop them on load to guarantee the session-only invariant. - Config.Tabs.RemoveAll(t => t.IsTempTab); - - // 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) + // Group A: Font + Theme parallel (Q1=A). Both are CPU-bound, + // independent, and dominate the load-time profile. Everything + // else stays sequential to keep ordering simple. + // Q3=B transition: BuildFonts() is sync today; Task 5 converts + // FontManager itself to BuildFontsAsync. + var fontTask = Task.Run(() => { - 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"); + FontManager = new FontManager(); + FontManager.BuildFonts(); + }, cancellationToken); - 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) + var themeTask = Task.Run(() => { - 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)"); - } + // v1.1.0 — Theme-Engine init. Custom-Themes liegen in + // pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get. + var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(customThemesDir); + SeedExampleThemeIfEmpty(customThemesDir); + ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); + ThemeRegistry.Switch(Config.Theme); + }, cancellationToken); - // 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 - // and gameplay-event noise; FreeCompany, Group and Linkshell each - // own their respective channel set. Tells are not in a static - // tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation - // tabs on demand. Novice-Network gets no preset tab; users who - // want it can add HellionBeginner from Settings → Tabs. - if (Config.Tabs.Count == 0) - { - Config.Tabs.Add(TabsUtil.VanillaGeneral); - Config.Tabs.Add(TabsUtil.HellionSystem); - Config.Tabs.Add(TabsUtil.HellionFreeCompany); - Config.Tabs.Add(TabsUtil.HellionParty); - Config.Tabs.Add(TabsUtil.HellionLinkshell); - } - - LanguageChanged(Interface.UiLanguage); - ImGuiUtil.Initialize(this); - - FileDialogManager = new FileDialogManager(); - - Commands = new Commands(); - Functions = new GameFunctions.GameFunctions(this); - Ipc = new IpcManager(); - TypingIpc = new TypingIpc(this); - ExtraChat = new ExtraChat(); - FontManager = new FontManager(); - - // v1.1.0 — Theme-Engine init. Custom-Themes liegen in - // pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get. - var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); - Directory.CreateDirectory(customThemesDir); - SeedExampleThemeIfEmpty(customThemesDir); - ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); - ThemeRegistry.Switch(Config.Theme); + await Task.WhenAll(fontTask, themeTask).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); // SelfTest hooks live alongside the live registry — the steps // poll Active per frame and need the registry already wired. @@ -461,15 +517,7 @@ public sealed class Plugin : IDalamudPlugin new SelfTests.ThemeSwitchSelfTestStep(this), ]); - // Plugin integrations register their IPC subscribers up-front so - // Ready/Disposing events from the target plugins are caught from - // the very first frame, even if the user's Honorific reloads - // mid-session. See HellionChat/Integrations/HonorificService.cs. - HonorificService = new Integrations.HonorificService(Interface, Log, Framework); - - StatusBar = new Ui.StatusBar(); - - MessageManager = new MessageManager(this); // Does it require UI? + MessageManager = new MessageManager(this); // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the // MessageManager's MessageProcessed event for live tells and @@ -479,11 +527,6 @@ public sealed class Plugin : IDalamudPlugin AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store); AutoTellTabsService.Initialize(); - // Hellion Chat — daily retention sweep, off-thread so it never - // blocks plugin load. Skips itself when disabled or already ran - // within the past 24 hours. - RunRetentionSweepIfDue(); - ChatLogWindow = new ChatLogWindow(this); SettingsWindow = new SettingsWindow(this); DbViewer = new DbViewer(this); @@ -507,17 +550,24 @@ public sealed class Plugin : IDalamudPlugin if (!Config.FirstRunCompleted) FirstRunWizard.IsOpen = true; - FontManager.BuildFonts(); - - Interface.UiBuilder.DisableCutsceneUiHide = true; - Interface.UiBuilder.DisableGposeUiHide = true; - // let all the other components register, then initialize commands Commands.Initialise(); if (Interface.Reason is not PluginLoadReason.Boot) MessageManager.FilterAllTabsAsync(); + #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(); + #endif + + cancellationToken.ThrowIfCancellationRequested(); + + // (B1) Hooks last: every service and window must be live before + // Dalamud fires our first Draw / FrameworkUpdate tick. Anything + // earlier risks rendering against null FontManager / ThemeRegistry. Framework.Update += FrameworkUpdate; Interface.UiBuilder.Draw += Draw; Interface.LanguageChanged += LanguageChanged; @@ -526,68 +576,121 @@ public sealed class Plugin : IDalamudPlugin // most useful landing place; OpenConfigUi is already wired to // the same toggle inside SettingsWindow. Interface.UiBuilder.OpenMainUi += OpenMainUi; - - if (Config.ShowEmotes) - _ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside - - #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(); - #endif } - catch (Exception ex) + catch { - Log.Error(ex, "Plugin load threw an error, turning off plugin"); - Dispose(); - - // Re-throw the exception to fail the plugin load. + // Mirror the v1.4.0 load-failure recovery: hand off to DisposeAsync + // so partially-built services are torn down. Swallow the cleanup + // exception so the original load failure stays the visible cause. + try { await DisposeAsync().ConfigureAwait(false); } + catch { /* keep original failure */ } throw; } } - // Suppressing this warning because Dispose() is called in Plugin() if the - // load fails, so some values may not be initialized. + // Suppressing this warning because DisposeAsync may run after a partial + // LoadAsync, so some properties may not be initialized. [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract")] - public void Dispose() + public async ValueTask DisposeAsync() { - Interface.UiBuilder.OpenMainUi -= OpenMainUi; - Interface.LanguageChanged -= LanguageChanged; - Interface.UiBuilder.Draw -= Draw; - Framework.Update -= FrameworkUpdate; - GameFunctions.GameFunctions.SetChatInteractable(true); + // (B3) Idempotency guard — Dalamud may reload-race us; second + // call short-circuits so we don't double-dispose services. + if (Interlocked.Exchange(ref _disposeStarted, 1) != 0) + return; - // FrameworkUpdate would have fired the pending save in N frames, - // but we just unsubscribed it. -1 is the idle sentinel. - if (DeferredSaveFrames >= 0) + Exception? failure = null; + + // Hooks unsubscribe FIRST so no Draw / FrameworkUpdate / LanguageChanged + // tick can fire while we're tearing services down. Mirrors the + // hooks-last subscribe order in LoadAsync. + failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi); + failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged); + failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw); + failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate); + + // v1.4.0 F5.3 — flush a pending DeferredSave before service teardown, + // since FrameworkUpdate just got unsubscribed and won't fire it. + failure = CaptureFailure(failure, () => { - SaveConfig(); - DeferredSaveFrames = -1; + if (DeferredSaveFrames >= 0) + { + SaveConfig(); + DeferredSaveFrames = -1; + } + }); + + // Auto-Tell-Tabs unsubscribes from MessageProcessed before MessageManager + // goes away. Pure-memory cleanup, no framework-thread pflicht. + failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose()); + + // v1.4.0 F6.2 — MessageManager has its own async dispose path + // (DB flush, pending-message thread shutdown). Run it before the + // framework-block so the worker threads are quiesced first. + if (MessageManager is not null) + { + failure = await CaptureFailureAsync(failure, () => MessageManager.DisposeAsync().AsTask()) + .ConfigureAwait(false); } - HonorificService?.Dispose(); + // (B4) Game-Function / IPC / UI-Window cleanup MUST run on the + // framework thread. WindowSystem mutations and IPC subscriber + // disposes touch Dalamud state that's only safe from the framework. + // Worker-thread DisposeAsync would race the next Draw tick. + failure = await CaptureFailureAsync(failure, async () => + { + await Framework.RunOnFrameworkThread(() => + { + // Game-Functions first — other services may still query + // chat-interactable state during their Dispose. + try { GameFunctions.GameFunctions.SetChatInteractable(true); } catch { /* swallowed */ } - WindowSystem?.RemoveAllWindows(); - ChatLogWindow?.Dispose(); - DbViewer?.Dispose(); - InputPreview?.Dispose(); - SettingsWindow?.Dispose(); - DebuggerWindow?.Dispose(); - SeStringDebugger?.Dispose(); + // IPC subscribers — dispose before windows so any final + // event firing from the IPC source can't reach a half-torn + // ChatLogWindow. + HonorificService?.Dispose(); + TypingIpc?.Dispose(); + ExtraChat?.Dispose(); + Ipc?.Dispose(); - TypingIpc?.Dispose(); - ExtraChat?.Dispose(); - Ipc?.Dispose(); - // Dispose the Auto-Tell-Tabs service before MessageManager so it - // can cleanly unsubscribe from the MessageProcessed event before - // its source goes away. - AutoTellTabsService?.Dispose(); - MessageManager?.DisposeAsync().AsTask().Wait(); - Functions?.Dispose(); - Commands?.Dispose(); + // Windows — RemoveAllWindows first, then per-window Dispose. + // Order matches the pre-v1.4.3 Dispose body byte-for-byte. + // CommandHelpWindow and FirstRunWizard don't implement + // IDisposable; their resources are reclaimed via WindowSystem. + WindowSystem?.RemoveAllWindows(); + ChatLogWindow?.Dispose(); + DbViewer?.Dispose(); + InputPreview?.Dispose(); + SettingsWindow?.Dispose(); + DebuggerWindow?.Dispose(); + SeStringDebugger?.Dispose(); + }).ConfigureAwait(false); + }).ConfigureAwait(false); - EmoteCache.Dispose(); + // Pure-memory cleanups — no Framework / UI / IPC touch, so they + // run on whatever thread DisposeAsync resumes on. + failure = CaptureFailure(failure, () => Functions?.Dispose()); + failure = CaptureFailure(failure, () => Commands?.Dispose()); + failure = CaptureFailure(failure, () => EmoteCache.Dispose()); + + if (failure is not null) + ExceptionDispatchInfo.Capture(failure).Throw(); + } + + // Lightless-pattern capture helpers: run cleanup, remember the FIRST + // exception, keep going. Without these one mid-teardown failure would + // skip every cleanup behind it and leave services half-torn. + private static Exception? CaptureFailure(Exception? failure, Action action) + { + try { action(); } + catch (Exception ex) { failure ??= ex; } + return failure; + } + + private static async ValueTask CaptureFailureAsync(Exception? failure, Func action) + { + try { await action().ConfigureAwait(false); } + catch (Exception ex) { failure ??= ex; } + return failure; } // Reads HellionThemeWindowOpacity from the pre-v13 backup the v12→v13 From daa800c8b16d3946988c457ef537185874fb7fa7 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 19:46:11 +0200 Subject: [PATCH 02/10] Apply code-quality fixes to Plugin.cs IAsyncDalamudPlugin refactor I-1: rewrite property-shape comment to reflect that all properties (not just Phase-2 ones) moved to { get; private set; } = null!;. I-3: drop plan-jargon (Q1=A / Q3=B / Task 5) from source comments; replace with durable rationale and a version-anchored TODO for the FontManager.BuildFontsAsync follow-up. I-4: remove German-word leak ("pflicht") from English comment in DisposeAsync. M-5: wrap each cleanup line inside Framework.RunOnFrameworkThread with CaptureFailure so a single Dispose throw no longer strands subsequent cleanup. Drops the inline try/swallow on SetChatInteractable. Mirrors Lightless DisposeFrameworkBoundServicesAsync pattern. --- HellionChat/Plugin.cs | 52 ++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 0d05778..dc14cf3 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -49,9 +49,10 @@ public sealed class Plugin : IAsyncDalamudPlugin public readonly WindowSystem WindowSystem = new(PluginName); - // v1.4.3: Phase-2 services need private setters now that LoadAsync - // owns their construction. Phase-1-only services (Commands, Functions, - // Ipc, ExtraChat, TypingIpc) keep their setters for symmetry. + // v1.4.3: properties moved from { get; } to { get; private set; } = null!; + // because LoadAsync now owns construction of the Phase-2 services. + // Phase-1 services use the same shape for consistency, even though + // they're still allocated in the ctor. public SettingsWindow SettingsWindow { get; private set; } = null!; public ChatLogWindow ChatLogWindow { get; private set; } = null!; public DbViewer DbViewer { get; private set; } = null!; @@ -486,14 +487,13 @@ public sealed class Plugin : IAsyncDalamudPlugin try { - // Group A: Font + Theme parallel (Q1=A). Both are CPU-bound, - // independent, and dominate the load-time profile. Everything - // else stays sequential to keep ordering simple. - // Q3=B transition: BuildFonts() is sync today; Task 5 converts - // FontManager itself to BuildFontsAsync. + // Group A: Font + Theme parallel — both CPU-bound, independent, and + // dominate the load-time profile. Everything else stays sequential to + // keep ordering simple. var fontTask = Task.Run(() => { FontManager = new FontManager(); + // TODO(v1.4.x): replace with FontManager.BuildFontsAsync(cancellationToken) FontManager.BuildFonts(); }, cancellationToken); @@ -620,7 +620,7 @@ public sealed class Plugin : IAsyncDalamudPlugin }); // Auto-Tell-Tabs unsubscribes from MessageProcessed before MessageManager - // goes away. Pure-memory cleanup, no framework-thread pflicht. + // goes away. Pure-memory cleanup, no framework-thread requirement. failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose()); // v1.4.0 F6.2 — MessageManager has its own async dispose path @@ -636,35 +636,41 @@ public sealed class Plugin : IAsyncDalamudPlugin // framework thread. WindowSystem mutations and IPC subscriber // disposes touch Dalamud state that's only safe from the framework. // Worker-thread DisposeAsync would race the next Draw tick. - failure = await CaptureFailureAsync(failure, async () => + // Per-line CaptureFailure so a single throw can't strand the lines + // behind it; mirrors Lightless DisposeFrameworkBoundServicesAsync. + try { await Framework.RunOnFrameworkThread(() => { // Game-Functions first — other services may still query // chat-interactable state during their Dispose. - try { GameFunctions.GameFunctions.SetChatInteractable(true); } catch { /* swallowed */ } + failure = CaptureFailure(failure, () => GameFunctions.GameFunctions.SetChatInteractable(true)); // IPC subscribers — dispose before windows so any final // event firing from the IPC source can't reach a half-torn // ChatLogWindow. - HonorificService?.Dispose(); - TypingIpc?.Dispose(); - ExtraChat?.Dispose(); - Ipc?.Dispose(); + failure = CaptureFailure(failure, () => HonorificService?.Dispose()); + failure = CaptureFailure(failure, () => TypingIpc?.Dispose()); + failure = CaptureFailure(failure, () => ExtraChat?.Dispose()); + failure = CaptureFailure(failure, () => Ipc?.Dispose()); // Windows — RemoveAllWindows first, then per-window Dispose. // Order matches the pre-v1.4.3 Dispose body byte-for-byte. // CommandHelpWindow and FirstRunWizard don't implement // IDisposable; their resources are reclaimed via WindowSystem. - WindowSystem?.RemoveAllWindows(); - ChatLogWindow?.Dispose(); - DbViewer?.Dispose(); - InputPreview?.Dispose(); - SettingsWindow?.Dispose(); - DebuggerWindow?.Dispose(); - SeStringDebugger?.Dispose(); + failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows()); + failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose()); + failure = CaptureFailure(failure, () => DbViewer?.Dispose()); + failure = CaptureFailure(failure, () => InputPreview?.Dispose()); + failure = CaptureFailure(failure, () => SettingsWindow?.Dispose()); + failure = CaptureFailure(failure, () => DebuggerWindow?.Dispose()); + failure = CaptureFailure(failure, () => SeStringDebugger?.Dispose()); }).ConfigureAwait(false); - }).ConfigureAwait(false); + } + catch (Exception ex) + { + failure ??= ex; + } // Pure-memory cleanups — no Framework / UI / IPC touch, so they // run on whatever thread DisposeAsync resumes on. From ccc5a4e17ad42f42881a725c4a4543d494188798 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 20:34:05 +0200 Subject: [PATCH 03/10] Add BuildFontsAsync for parallel font/theme init --- HellionChat/FontManager.cs | 13 +++++++++++++ HellionChat/Plugin.cs | 5 ++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index c9e11b4..07e94d5 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -100,6 +100,19 @@ public class FontManager JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges); } + /// + /// Async wrapper around for the Phase-1 LoadAsync + /// path. The font-atlas build is CPU-bound, so we offload via Task.Run and + /// honour the cancellation token at the scheduling boundary; this lets the + /// font build run in parallel with the theme init without blocking the + /// loader. Settings-driven manual rebuilds keep using the sync entry point. + /// + public async Task BuildFontsAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Run(BuildFonts, cancellationToken).ConfigureAwait(false); + } + public void BuildFonts() { SetUpRanges(); diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index dc14cf3..d6b3728 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -490,11 +490,10 @@ public sealed class Plugin : IAsyncDalamudPlugin // Group A: Font + Theme parallel — both CPU-bound, independent, and // dominate the load-time profile. Everything else stays sequential to // keep ordering simple. - var fontTask = Task.Run(() => + var fontTask = Task.Run(async () => { FontManager = new FontManager(); - // TODO(v1.4.x): replace with FontManager.BuildFontsAsync(cancellationToken) - FontManager.BuildFonts(); + await FontManager.BuildFontsAsync(cancellationToken).ConfigureAwait(false); }, cancellationToken); var themeTask = Task.Run(() => From b75c7b177acbf4ec3839f82c32f592a302dd416e Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 21:00:19 +0200 Subject: [PATCH 04/10] Move RunRetentionSweepIfDue to Phase 2 (depends on MessageManager.Store) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke test in Task 6 surfaced a NullReferenceException at Plugin.cs:885 — the retention sweep was scheduled in Phase 1 but dereferences MessageManager.Store, which is only allocated in Phase 2 (LoadAsync). Move the call after MessageManager init. Drop the comment that wrongly claimed independence from Phase-2 services. --- HellionChat/Plugin.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index d6b3728..47c8fae 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -471,12 +471,6 @@ public sealed class Plugin : IAsyncDalamudPlugin Interface.UiBuilder.DisableCutsceneUiHide = true; Interface.UiBuilder.DisableGposeUiHide = true; - // Hellion Chat — daily retention sweep, off-thread so it never - // blocks plugin load. Skips itself when disabled or already ran - // within the past 24 hours. Pure fire-and-forget on a background - // thread, so it doesn't depend on Phase-2 services being live. - RunRetentionSweepIfDue(); - if (Config.ShowEmotes) _ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside } @@ -518,6 +512,11 @@ public sealed class Plugin : IAsyncDalamudPlugin MessageManager = new MessageManager(this); + // Daily retention sweep, fire-and-forget. Lives in Phase 2 because + // it dereferences MessageManager.Store. Skips itself when disabled + // or when it already ran within the past 24 hours. + RunRetentionSweepIfDue(); + // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the // MessageManager's MessageProcessed event for live tells and // to ClientState.Logout for the cleanup pass. Created after From 0b25df0ea7dc88a68e58e66b2abcc3b82e402126 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 21:38:44 +0200 Subject: [PATCH 05/10] Move migrations and service allocations from Phase-1 ctor to LoadAsync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-1 was still doing 7 schema migrations and 25+ service allocations synchronously, blocking the ctor return. Move all of that to LoadAsync, keeping only bootstrap-essentials in the ctor: conflict detection, config load, language init, ImGui init, WindowSystem skeleton. Decouple the font task from the LoadAsync await — font-build runs fire-and-forget, so first frames render with Dalamud's default font until the Hellion-Exo2/NotoSans atlas rebuild completes (visible "font-pop"). Mirrors ChatTwo's pattern; the perceived-load win comes from "Finished loading" landing earlier, not from a faster atlas build. --- HellionChat/Plugin.cs | 750 +++++++++++++++++++++--------------------- 1 file changed, 379 insertions(+), 371 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 47c8fae..a31ab28 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -108,10 +108,11 @@ public sealed class Plugin : IAsyncDalamudPlugin public Plugin() { - // v1.4.3 Phase 1 — sync ctor: only work that doesn't touch - // Theme/Font/MessageManager/UI-windows lives here. Async Phase 2 - // (LoadAsync) owns those. Hooks subscribe at the END of LoadAsync - // so Dalamud's first Draw tick never sees null services (B1). + // Phase-1 ctor stays minimal: bootstrap-essentials only (conflict + // gate, config load, language + ImGui init, WindowSystem skeleton). + // Schema migrations and every service / window allocation moved to + // LoadAsync so the sync ctor returns fast. On failure here nothing + // is initialized yet, so just throw — there is nothing to clean up. // Refuse to start if upstream Chat 2 is loaded — prevents IPC // channel collisions and double-replacement of the in-game chat @@ -134,345 +135,13 @@ public sealed class Plugin : IAsyncDalamudPlugin // Drop them on load to guarantee the session-only invariant. Config.Tabs.RemoveAll(t => t.IsTempTab); - // 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 - // and gameplay-event noise; FreeCompany, Group and Linkshell each - // own their respective channel set. Tells are not in a static - // tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation - // tabs on demand. Novice-Network gets no preset tab; users who - // want it can add HellionBeginner from Settings → Tabs. - if (Config.Tabs.Count == 0) - { - Config.Tabs.Add(TabsUtil.VanillaGeneral); - Config.Tabs.Add(TabsUtil.HellionSystem); - Config.Tabs.Add(TabsUtil.HellionFreeCompany); - Config.Tabs.Add(TabsUtil.HellionParty); - Config.Tabs.Add(TabsUtil.HellionLinkshell); - } - LanguageChanged(Interface.UiLanguage); ImGuiUtil.Initialize(this); - FileDialogManager = new FileDialogManager(); + DeferredSaveFrames = -1; - // Phase-1 services: pure allocation, no Theme/Font/UI touch. - Commands = new Commands(); - Functions = new GameFunctions.GameFunctions(this); - Ipc = new IpcManager(); - TypingIpc = new TypingIpc(this); - ExtraChat = new ExtraChat(); - - // Plugin integrations register their IPC subscribers up-front so - // Ready/Disposing events from the target plugins are caught from - // the very first frame, even if the user's Honorific reloads - // mid-session. See HellionChat/Integrations/HonorificService.cs. - HonorificService = new Integrations.HonorificService(Interface, Log, Framework); - - StatusBar = new Ui.StatusBar(); - - Interface.UiBuilder.DisableCutsceneUiHide = true; - Interface.UiBuilder.DisableGposeUiHide = true; - - if (Config.ShowEmotes) - _ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside + // WindowSystem skeleton is initialised by the readonly field above — + // no AddWindow yet; window construction lives in LoadAsync. } public async Task LoadAsync(CancellationToken cancellationToken) @@ -481,50 +150,377 @@ public sealed class Plugin : IAsyncDalamudPlugin try { - // Group A: Font + Theme parallel — both CPU-bound, independent, and - // dominate the load-time profile. Everything else stays sequential to - // keep ordering simple. - var fontTask = Task.Run(async () => + // 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 + // and gameplay-event noise; FreeCompany, Group and Linkshell each + // own their respective channel set. Tells are not in a static + // tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation + // tabs on demand. Novice-Network gets no preset tab; users who + // want it can add HellionBeginner from Settings → Tabs. + if (Config.Tabs.Count == 0) + { + Config.Tabs.Add(TabsUtil.VanillaGeneral); + Config.Tabs.Add(TabsUtil.HellionSystem); + Config.Tabs.Add(TabsUtil.HellionFreeCompany); + Config.Tabs.Add(TabsUtil.HellionParty); + Config.Tabs.Add(TabsUtil.HellionLinkshell); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Font-build runs fire-and-forget. The atlas rebuild lands a few + // hundred ms after LoadAsync returns; first frames draw with + // Dalamud's default font until the Hellion-Exo2 / NotoSans handles + // are ready, then ImGui switches to the custom fonts (visible + // "font-pop"). Mirrors ChatTwo's pattern — perceived-load win + // comes from "Finished loading" landing earlier, not from a faster + // atlas build. + _ = Task.Run(async () => { FontManager = new FontManager(); await FontManager.BuildFontsAsync(cancellationToken).ConfigureAwait(false); }, cancellationToken); - var themeTask = Task.Run(() => - { - // v1.1.0 — Theme-Engine init. Custom-Themes liegen in - // pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get. - var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); - Directory.CreateDirectory(customThemesDir); - SeedExampleThemeIfEmpty(customThemesDir); - ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); - ThemeRegistry.Switch(Config.Theme); - }, cancellationToken); + // Theme init stays sync on the LoadAsync continuation — cheap, + // and Active is read every Draw frame, so the registry must be + // wired before the first hook fires. + var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(customThemesDir); + SeedExampleThemeIfEmpty(customThemesDir); + ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); + ThemeRegistry.Switch(Config.Theme); - await Task.WhenAll(fontTask, themeTask).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - // SelfTest hooks live alongside the live registry — the steps - // poll Active per frame and need the registry already wired. + // Service allocations: order encodes dependencies. Commands is + // alloc-only here; Initialise() runs after windows exist so the + // slash-commands can toggle their visibility. HonorificService + // registers IPC subscribers up-front so Ready/Disposing events + // are caught from the very first frame. + FileDialogManager = new FileDialogManager(); + Commands = new Commands(); + Functions = new GameFunctions.GameFunctions(this); + Ipc = new IpcManager(); + TypingIpc = new TypingIpc(this); + ExtraChat = new ExtraChat(); + HonorificService = new Integrations.HonorificService(Interface, Log, Framework); + StatusBar = new Ui.StatusBar(); + MessageManager = new MessageManager(this); + + // Auto-Tell-Tabs subscribes to MessageManager.MessageProcessed for + // live tells and to ClientState.Logout for cleanup; needs the live + // store handed in at construction. + AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store); + AutoTellTabsService.Initialize(); + + // SelfTest steps poll Active per frame and need the registry wired. SelfTestRegistry.RegisterTestSteps([ new SelfTests.ThemeSwitchSelfTestStep(this), ]); - MessageManager = new MessageManager(this); - - // Daily retention sweep, fire-and-forget. Lives in Phase 2 because - // it dereferences MessageManager.Store. Skips itself when disabled - // or when it already ran within the past 24 hours. - RunRetentionSweepIfDue(); - - // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the - // MessageManager's MessageProcessed event for live tells and - // to ClientState.Logout for the cleanup pass. Created after - // MessageManager so the constructor can hand off the live - // store and event source. - AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store); - AutoTellTabsService.Initialize(); - ChatLogWindow = new ChatLogWindow(this); SettingsWindow = new SettingsWindow(this); DbViewer = new DbViewer(this); @@ -548,12 +544,24 @@ public sealed class Plugin : IAsyncDalamudPlugin if (!Config.FirstRunCompleted) FirstRunWizard.IsOpen = true; + cancellationToken.ThrowIfCancellationRequested(); + // let all the other components register, then initialize commands Commands.Initialise(); + // Daily retention sweep, fire-and-forget. Skips itself when + // disabled or when it already ran within the past 24 hours. + RunRetentionSweepIfDue(); + + if (Config.ShowEmotes) + _ = EmoteCache.LoadData(); // Fire-and-forget, exceptions caught inside + if (Interface.Reason is not PluginLoadReason.Boot) MessageManager.FilterAllTabsAsync(); + Interface.UiBuilder.DisableCutsceneUiHide = true; + 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 From 5931f2f30141b84a3efbeb03bc3ae2715ef98419 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 21:42:57 +0200 Subject: [PATCH 06/10] Use sync FontManager allocation in LoadAsync to avoid first-draw race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fire-and-forget Task.Run pattern could leave Plugin.FontManager null when the first UiBuilder.Draw tick fires (ChatLogWindow dereferences FontManager.FontAwesome / RegularFont / ItalicFont in its draw paths). Allocate FontManager and call BuildFonts() synchronously, mirroring ChatTwo Plugin.cs:152. BuildFonts itself is non-blocking — it just registers IFontHandles with Dalamud's atlas; the actual atlas rebuild runs on Dalamud's pipeline a few frames later, so the perceived-load win still holds (LoadAsync no longer waits for atlas build). BuildFontsAsync in FontManager.cs stays for the Settings-driven manual rebuild path. --- HellionChat/Plugin.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index a31ab28..41c63a1 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -471,18 +471,15 @@ public sealed class Plugin : IAsyncDalamudPlugin cancellationToken.ThrowIfCancellationRequested(); - // Font-build runs fire-and-forget. The atlas rebuild lands a few - // hundred ms after LoadAsync returns; first frames draw with - // Dalamud's default font until the Hellion-Exo2 / NotoSans handles - // are ready, then ImGui switches to the custom fonts (visible - // "font-pop"). Mirrors ChatTwo's pattern — perceived-load win - // comes from "Finished loading" landing earlier, not from a faster - // atlas build. - _ = Task.Run(async () => - { - FontManager = new FontManager(); - await FontManager.BuildFontsAsync(cancellationToken).ConfigureAwait(false); - }, cancellationToken); + // Sync allocation + handle registration. BuildFonts() registers + // IFontHandles with Dalamud's UiBuilder.FontAtlas — registration + // itself is non-blocking (handles stored, lambdas queued). Dalamud + // rebuilds the atlas on its own pipeline a few frames later; first + // frames render with the default font until the rebuild lands and + // ImGui switches to Hellion-Exo2 / NotoSans (visible "font-pop"). + // Mirrors ChatTwo Plugin.cs:152. + FontManager = new FontManager(); + FontManager.BuildFonts(); // Theme init stays sync on the LoadAsync continuation — cheap, // and Active is read every Draw frame, so the registry must be From a1f2b22b193dfaf289ab04d3d457bd030bd7f4d2 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 21:59:29 +0200 Subject: [PATCH 07/10] 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; From baeec369e663426112acc9a0c67b785b8473b802 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 22:12:40 +0200 Subject: [PATCH 08/10] Cutover custom-repo URL from GitHub to Gitea --- HellionChat/HellionChat.yaml | 12 ++++++------ HellionChat/Ui/SettingsTabs/Information.cs | 2 +- NOTICE.md | 2 +- PRIVACY.md | 4 ++-- README.md | 11 +++++------ SECURITY.md | 14 +++++--------- SUPPORT.md | 12 ++++++------ docs/AI_DISCLOSURE.md | 2 +- docs/CHANGELOG.md | 18 +++++++++--------- docs/CONTRIBUTORS.md | 2 +- docs/ROADMAP.md | 2 +- repo.json | 18 +++++++++--------- 12 files changed, 47 insertions(+), 52 deletions(-) diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index ad3ec27..6e593d4 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -63,13 +63,13 @@ description: |- Modding & support: join the Hellion Forge Discord at https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and other Hellion Online Media plugins/tools. -repo_url: https://github.com/JonKazama-Hellion/HellionChat +repo_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat accepts_feedback: true -icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png +icon_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png image_urls: - - https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png - - https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/settingsOverview.png - - https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/themesPicker.png + - https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png + - https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png + - https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png tags: - Social - UI @@ -193,4 +193,4 @@ changelog: |- --- - Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases + Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases diff --git a/HellionChat/Ui/SettingsTabs/Information.cs b/HellionChat/Ui/SettingsTabs/Information.cs index c1c0096..121eeeb 100644 --- a/HellionChat/Ui/SettingsTabs/Information.cs +++ b/HellionChat/Ui/SettingsTabs/Information.cs @@ -76,7 +76,7 @@ internal sealed class Information : ISettingsTab ImGui.TextUnformatted(Language.Options_About_Github_Issues); ImGui.SameLine(); if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues")) - Dalamud.Utility.Util.OpenLink("https://github.com/JonKazama-Hellion/HellionChat/issues"); + Dalamud.Utility.Util.OpenLink("https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues"); } } diff --git a/NOTICE.md b/NOTICE.md index 9043229..8274bf5 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -58,7 +58,7 @@ is below. If something in HellionChat causes problems, especially if it relates back to Chat 2 or to anything Infi or Anna would want flagged: -- **GitHub Issues:** [JonKazama-Hellion/HellionChat/issues](https://github.com/JonKazama-Hellion/HellionChat/issues) +- **Gitea Issues:** [JonKazama-Hellion/HellionChat/issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues) - **Discord:** `@j.j_kazama` - **Email (business):** kontakt@hellion-media.de diff --git a/PRIVACY.md b/PRIVACY.md index 547a5a0..ba09555 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -121,10 +121,10 @@ Adjust the channel whitelist or set retention to a low value. Both take effect i | Party | Why they appear | What reaches them | Their privacy policy | | --- | --- | --- | --- | | BetterTTV (NightDev LLC) | Optional emote rendering | HTTPS request for an emote ID; your IP | | -| GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | | +| Hellion Forge (Gitea, self-hosted by Hellion Online Media) | Plugin distribution via custom repo, issue tracker | Whatever the Gitea instance sees from any HTTPS request to a public repo | | | Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | | -GitHub and the Dalamud/XIVLauncher loader are unavoidable for anyone playing FFXIV through Dalamud at all. BetterTTV is the only third party HellionChat introduces on top of that baseline, and it is opt-out via settings. +The Hellion Forge Gitea instance and the Dalamud/XIVLauncher loader are unavoidable for anyone using HellionChat through Dalamud at all. BetterTTV is the only third party HellionChat introduces on top of that baseline, and it is opt-out via settings. --- diff --git a/README.md b/README.md index b01f483..447fa5e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # Hellion Chat -[![Build](https://github.com/JonKazama-Hellion/HellionChat/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/JonKazama-Hellion/HellionChat/actions/workflows/build.yml) -[![CodeQL](https://github.com/JonKazama-Hellion/HellionChat/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/JonKazama-Hellion/HellionChat/security/code-scanning) +[![Build](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml/badge.svg?branch=main)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml) [![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE) -[![Latest release](https://img.shields.io/github/v/release/JonKazama-Hellion/HellionChat?display_name=tag&sort=semver&color=brightgreen)](https://github.com/JonKazama-Hellion/HellionChat/releases/latest) +[![Latest release](https://img.shields.io/badge/release-v1.4.2-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest) [![Dalamud API](https://img.shields.io/badge/Dalamud-API_15-purple)](https://github.com/goatcorp/Dalamud) [![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) [![FFXIV](https://img.shields.io/badge/FFXIV-Dawntrail-c3a37f)](https://www.finalfantasyxiv.com/) @@ -170,7 +169,7 @@ Hellion Chat wird über ein Dalamud-**Custom-Repository** verteilt. 1. Dalamud-Settings (`/xlsettings`) → **Experimental** öffnen. 2. Neuen Eintrag unter **Custom Plugin Repositories** anlegen: ``` - https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json + https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json ``` 3. **Save**, dann in `/xlplugins` → **All Plugins** → Refresh. 4. Hellion Chat taucht in der Liste auf, dann installieren wie jedes andere Plugin. @@ -244,7 +243,7 @@ Hellion Chat ist ein eigenständiges Plugin, kein Fork mehr im Repository-Sinne. - Theme-Engine mit zehn eingebauten Themes plus JSON-Authoring-Format (Engine v1.1.0, Katalog erweitert in v1.2.3, inkl. CVD-safe Hellion Spectrum; Synthwave Sunset in v1.4.1) - ABGR-Cache auf den Theme-Records: HellionStyle.PushGlobal liest pre-computed ABGR statt RGBA→ABGR pro Slot pro Frame (v1.4.1, ~13 % Render-Time-Recovery) -In Arbeit: schrittweise Modernisierung des UI-Look-and-Feel über die Theme-Engine hinaus. Was als Nächstes geplant ist und welche Themen langfristig auf der Liste stehen, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md). Konkrete eingeplante Items werden zusätzlich im [GitHub-Issue-Tracker](https://github.com/JonKazama-Hellion/HellionChat/issues) mit dem `roadmap`-Label geführt. +In Arbeit: schrittweise Modernisierung des UI-Look-and-Feel über die Theme-Engine hinaus. Was als Nächstes geplant ist und welche Themen langfristig auf der Liste stehen, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md). Konkrete eingeplante Items werden zusätzlich im [Gitea-Issue-Tracker](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues) mit dem `roadmap`-Label geführt. ### Zur Release-Kadenz @@ -255,7 +254,7 @@ Wer den Repo zum ersten Mal sieht, bemerkt schnell viele Releases und sehr viele ## Community und Support - **Hellion Forge Discord** (Modding- und Plugin-Community von Hellion Online Media): https://discord.gg/X9V7Kcv5gR -- Bug-Reports und Feature-Requests: [GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues) +- Bug-Reports und Feature-Requests: [Gitea Issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues) - Discord DM: `@j.j_kazama` - Weitere Kontaktwege (Security, Privacy, Quick-Questions): siehe [SUPPORT.md](SUPPORT.md) diff --git a/SECURITY.md b/SECURITY.md index 783e2e1..4d060f4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,24 +3,20 @@ ## Reporting a Vulnerability If you find a security issue in HellionChat, please do not open a -public GitHub issue. Use one of the private channels below so I can +public Gitea issue. Use one of the private channels below so I can investigate and ship a fix before the details go public. **Preferred:** -[Privately report a vulnerability](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new) -via GitHub Security Advisories. This routes the report directly to me -and keeps the conversation off the public timeline. - -**Alternative:** | Channel | Address | | ---------- | -------------------------- | | Email | `kontakt@hellion-media.de` | | Discord DM | `@j.j_kazama` | -I respond on weekdays during European business hours. For urgent -disclosures (active exploitation, user-data exposure) email is the -fastest path. +For urgent disclosures (active exploitation, user-data exposure) email +is the fastest path. + +I respond on weekdays during European business hours. ## Scope diff --git a/SUPPORT.md b/SUPPORT.md index 03a15c8..51f6188 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -4,19 +4,19 @@ HellionChat is a small hobby project maintained by one person. There are a few d ## Bugs and feature requests -GitHub issues, using the templates: +Gitea issues, using the templates: -- [Bug report](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=bug_report.yml) -- [Feature request](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=feature_request.yml) +- [Bug report](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues/new?template=bug_report.yml) +- [Feature request](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues/new?template=feature_request.yml) -Please search [existing issues](https://github.com/JonKazama-Hellion/HellionChat/issues?q=is%3Aissue) first. Duplicates get closed and pointed at the original. +Please search [existing issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues?type=issue) first. Duplicates get closed and pointed at the original. ## Security Do **not** open a public issue for security-relevant findings. Use the private advisory route described in [SECURITY.md](SECURITY.md): -- [Private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new) -- Email `kontakt@hellion-media.de` +- Email `kontakt@hellion-media.de` (preferred for security reports) +- Discord DM `@j.j_kazama` for time-sensitive findings ## Privacy questions diff --git a/docs/AI_DISCLOSURE.md b/docs/AI_DISCLOSURE.md index 495d00a..2326882 100644 --- a/docs/AI_DISCLOSURE.md +++ b/docs/AI_DISCLOSURE.md @@ -82,4 +82,4 @@ Both are good projects. Use what fits you best. ## Contact Questions about this disclosure: - + diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1110992..5df4288 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,7 +5,7 @@ sich an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/), die Version-Nummern folgen [Semantischer Versionierung](https://semver.org/lang/de/). Detaillierte Release-Notes pro Version stehen direkt am -[GitHub-Release](https://github.com/JonKazama-Hellion/HellionChat/releases) +[Gitea-Release](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases) und im Plugin-Changelog-Block (`HellionChat/HellionChat.yaml` → `changelog:`). Diese Datei fasst die Releases als Überblick zusammen und verlinkt für Details auf die Release-Pages. @@ -279,7 +279,7 @@ Vier kleine Polish-Items aus dem Backlog gebündelt: auf Verbose-Level. Aus by default, Aktivierung via `/xllog set HellionChat verbose` für Bug-Report-Diagnose. -[Release-Notes 1.0.3](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.3) +[Release-Notes 1.0.3](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.0.3) ## [1.0.1] — 2026-05-04 — Window Position Recovery @@ -295,7 +295,7 @@ Bundled housekeeping since v1.0.0: documentation restructured into parser library bumped from 3.3.0 to 3.5.1, GitHub Actions bumps for `actions/setup-dotnet` (4 → 5) and `github/codeql-action` (3 → 4). -[Release-Notes 1.0.1](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.1) +[Release-Notes 1.0.1](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.0.1) ## [1.0.0] — 2026-05-03 — Standalone Major Release @@ -308,7 +308,7 @@ User auf Config-Version 12 oder älter neu strukturiert (5 thematische Tabs statt 6+ kitchen-sink). Sweep aus Critical- und Major-Findings aus dem Codebase-Audit eingearbeitet. -[Release-Notes 1.0.0](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.0) +[Release-Notes 1.0.0](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.0.0) ## [0.6.1] — 2026-05-03 — Pop-Out Discoverability & /tell Auto-Pop-Out @@ -318,7 +318,7 @@ Pop-Out öffnen". Pop-Out-Input ist jetzt standardmäßig aktiv. Bugfixes: Ghost-Windows bei LRU-Drop / Logout, Dead-Zone unter dem Input-Bar bei aktivem Hint-Banner. -[Release-Notes 0.6.1](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.6.1) +[Release-Notes 0.6.1](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.6.1) ## [0.6.0] — 2026-05-03 — UX Polish: Pop-Out Input + Colour Presets @@ -328,7 +328,7 @@ Text-Buffer pro Pop-Out. Sieben Built-in-Color-Presets (Klassik, High-Contrast, Pastell, Dark-Mode-Tuned, Hellion, Night Blue, Indigo Violet) zum One-Click-Apply. Konfigurations-Migration v10 → v11. -[Release-Notes 0.6.0](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.6.0) +[Release-Notes 0.6.0](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.6.0) ## [0.5.4] — 2026-05-02 — WrapText Hardening @@ -338,7 +338,7 @@ CodeQL-Critical-Alert "unvalidated local pointer arithmetic" dauerhaft. Keine nutzersichtbare Verhaltensänderung — Word-Wrap-Output ist byte-identisch zu 0.5.3. -[Release-Notes 0.5.4](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.5.4) +[Release-Notes 0.5.4](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.5.4) ## [0.5.3] — 2026-05-02 — Pointer Arithmetic Hardening @@ -346,7 +346,7 @@ Erster Anlauf zur Schließung des CodeQL-Critical-Alerts in `ImGuiUtil.WrapText`. Encoded-Byte-Buffer-Length wird vor der Pointer-Arithmetik via `GetByteCount` validiert. -[Release-Notes 0.5.3](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.5.3) +[Release-Notes 0.5.3](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.5.3) --- @@ -355,7 +355,7 @@ Pointer-Arithmetik via `GetByteCount` validiert. Releases vor 0.5.3 (Bootstrap-Phase 0.1.0 bis 0.5.2) sind direkt am GitHub-Release-Stream einsehbar: -[Alle Releases](https://github.com/JonKazama-Hellion/HellionChat/releases) +[Alle Releases](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases) --- diff --git a/docs/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md index 30499e7..4bc978c 100644 --- a/docs/CONTRIBUTORS.md +++ b/docs/CONTRIBUTORS.md @@ -55,7 +55,7 @@ Die Upstream-Sprach-Dateien (`Language..resx`) sind nicht Teil dieser Date ## Wie du beitragen kannst -Bug-Reports, Feature-Wünsche und Pull-Requests laufen über [GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues). Workflow und Erwartungen stehen in [`../CONTRIBUTING.md`](../CONTRIBUTING.md), Code of Conduct in [`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md). +Bug-Reports, Feature-Wünsche und Pull-Requests laufen über [Gitea Issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues). Workflow und Erwartungen stehen in [`../CONTRIBUTING.md`](../CONTRIBUTING.md), Code of Conduct in [`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md). Tester-Pool für neue Versionen läuft über den Hellion-Forge-Discord: [discord.gg/X9V7Kcv5gR](https://discord.gg/X9V7Kcv5gR). Wer in den Tester-Channel rein will, einfach im Forge melden. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9abaa0d..2879f97 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -3,7 +3,7 @@ Geplante Arbeit nach dem v1.0.0 Standalone-Cut. Diese Liste ist absichtlich grob: konkrete Specs, Größenschätzungen und Repro-Steps liegen im internen Backlog. Tracking nach außen läuft über -[GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues) +[Gitea Issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues) mit dem `roadmap`-Label, sobald ein Item für einen Cycle eingeplant ist. Reihenfolge ist Priorität, nicht Garantie. Items können sich verschieben diff --git a/repo.json b/repo.json index ec06edc..1f72dca 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "AssemblyVersion": "1.4.2.0", "Description": "Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally removed (the optional webinterface) and a stack of privacy controls is added on top. Tabs, channel filters, RGB colours, emotes, screenshot mode, IPC integration and the chat replacement window itself work the same. The webinterface is intentionally not part of Hellion Chat because it serves a different use case from the smaller default footprint this plugin is built around.\n\nOn top of that, Hellion Chat adds privacy and data-handling controls designed to align with the modern data protection rules that apply across the EU, the United States and Japan. By default only your own conversations are stored; messages from strangers, NPCs and system spam stay out of the database. Retention windows are configurable per channel, history can be wiped retroactively, and stored data can be exported on demand.\n\nKey privacy and data-handling features:\n\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with a Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three preset profiles (Privacy-First, Casual, Full History)\n- Bilingual UI (English and German) with live language switching\n- Independent plugin state — own config file and database directory, so Hellion Chat does not share state with upstream Chat 2\n\nv1.3.0 First plugin integration cycle. Honorific custom titles are shown in the chat header above the message log, with auto-detect and silent fallback when Honorific is not installed.\n\nv1.4.0 — Critical Lifecycle Fixes. Plugin reload and shutdown are cleaner: SQLite no longer leans on GC pressure to release its file, worker threads are explicitly background, deferred config saves no longer get lost mid-disable, and pre-v13 config backups carry the user's custom theme opacity into the v14 schema instead of falling back to the default.\n\nv1.4.1 — Theme Engine Performance plus a tenth built-in. HellionStyle.PushGlobal reads pre-computed ABGR values from a per-theme cache instead of converting RGBA per slot per frame (~13 % render-time recovery in typical scenes). Custom-theme hot-reload survives transient file locks (editor mid-save keeps the last-known-good snapshot). Synthwave Sunset joins as the tenth built-in theme — Hot Magenta + Cyan on midnight violet, 80s neon-grid vibes.\n\nv1.4.2 — ChatLog Frame-Hot-Path. Three per-frame allocation patterns gone from the chat-log render path: card-mode borders hoist invariants out of the per-message loop, auto-tell tab tint and icon get a per-tab cache, and the status bar gates its tab aggregation behind the same one-second cache it uses for the format strings.\n\nBased on Chat 2 by Infi and Anna, licensed under EUPL-1.2.\n\nModding & support: join the Hellion Forge Discord at https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and other Hellion Online Media plugins/tools.", "ApplicableVersion": "any", - "RepoUrl": "https://github.com/JonKazama-Hellion/HellionChat", + "RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat", "Tags": [ "Social", "UI", @@ -20,17 +20,17 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)", - "Changelog": "**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**\n\nThird sub-patch of the v1.4.x Polish Sweep series. Per-frame allocations from the chat-log render path eliminated.\n\n- DrawMessages card-mode hoists theme/drawList/winLeft/winRight/borderColorAbgr out of the per-message loop. About 500 redundant calls per frame at 100 visible messages, multiplied by every pop-out window\n- Auto-tell tab tint and icon use a per-tab cache. Hash computation and string allocation only happen when the tell target name or world drifts. AutoTellTabTint stays a pure hash helper; cache lives in a thin TabTintCache wrapper\n- Status bar gates its tab aggregation behind the same one-second cache it already used for the format strings. LINQ Sum and Count replaced with a single foreach pass that runs on roughly 1% of frames\n\nRealistic frame-time recovery: 2-5% in typical scenes, more on pop-out-heavy setups because the card-border hoist scales per window.\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.4.1 — Theme Engine Performance**\n\nSecond sub-patch of the v1.4.x Polish Sweep series. Heap pressure from the theme engine's per-frame render path removed, plus a tenth built-in theme and hardening for the custom-theme hot-reload.\n\n- Theme records carry a pre-computed ABGR-packed cache for every color slot; cache is filled when the theme is registered and refreshed defensively on every Switch()\n- HellionStyle.PushGlobal reads ABGR values from the cache instead of calling ColourUtil.RgbaToAbgr per slot per frame; ~13 % render-time recovery measured in typical scenes (plan estimate was 2–6 %, real ~10–15 %)\n- ThemeRegistry custom-theme reload distinguishes a recoverable file lock (editor mid-save) from a permanent IO failure; locked themes keep their last-known-good snapshot and retry on the next lookup instead of dropping out of the picker\n- New built-in: Synthwave Sunset — Hot Magenta + Cyan on midnight violet, 80s neon-grid vibes; tenth theme in the picker\n- Author credits refreshed: brand themes are credited as \"Hellion Forge\"; Mint Grove and Forge Merchantman now credited to Carla Beleandis as a community thanks\n\nNo schema bump, no user-visible behaviour change other than smoother frames on GC-sensitive setups and one additional colour option.\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.4.0 — Critical Lifecycle Fixes**\n\nFirst sub-patch of the v1.4.x Polish Sweep series. Seven known lifecycle and race bugs eliminated before any performance refactor sits on top.\n\n- MessageStore disposal no longer triggers GC.Collect globally; Pooling=false on the SQLite connection means there's nothing left to clean up by hand\n- PendingMessage and RetentionSweep worker threads are explicitly marked IsBackground=true so the plugin domain can unload during XIVLauncher reload without waiting for them\n- EmoteCache image and gif loaders moved from async-void to async Task with a shared task tracker, draining on Dispose so an in-flight load can no longer write to a disposed EmoteImages entry\n- DisposeAsync 10s timeout now warns loudly instead of silently leaving the worker behind\n- Plugin.Dispose flushes any pending DeferredSaveFrames before tearing services down, so settings changes made in the last few frames before disable are no longer lost\n- The v13→v14 config migration now reads the pre-v13 backup and carries HellionThemeWindowOpacity into the new WindowOpacity field instead of falling back to the default 0.85\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.3.0 - Plugin Integrations: Honorific**\n\nFirst step on the plugin-integration roadmap. HellionChat now listens to Honorific and shows your custom title in the chat header. The slot auto-hides when Honorific is not installed, when no custom title is active, or when you are using the original FFXIV title.\n\n- New \"Integrations\" settings tab\n- Honorific integration with auto-detection and live updates\n- \"Coming soon\" preview of the next five planned integrations: context menu actions, smart notifications, RP status block, ExtraChat channels, and quick DM compose\n- Maintainer attribution buttons for Honorific repo and Caraxi\n- New service-class pattern under HellionChat/Integrations/\n\nModding and support: join Hellion Forge - https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", + "Changelog": "**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**\n\nThird sub-patch of the v1.4.x Polish Sweep series. Per-frame allocations from the chat-log render path eliminated.\n\n- DrawMessages card-mode hoists theme/drawList/winLeft/winRight/borderColorAbgr out of the per-message loop. About 500 redundant calls per frame at 100 visible messages, multiplied by every pop-out window\n- Auto-tell tab tint and icon use a per-tab cache. Hash computation and string allocation only happen when the tell target name or world drifts. AutoTellTabTint stays a pure hash helper; cache lives in a thin TabTintCache wrapper\n- Status bar gates its tab aggregation behind the same one-second cache it already used for the format strings. LINQ Sum and Count replaced with a single foreach pass that runs on roughly 1% of frames\n\nRealistic frame-time recovery: 2-5% in typical scenes, more on pop-out-heavy setups because the card-border hoist scales per window.\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.4.1 — Theme Engine Performance**\n\nSecond sub-patch of the v1.4.x Polish Sweep series. Heap pressure from the theme engine's per-frame render path removed, plus a tenth built-in theme and hardening for the custom-theme hot-reload.\n\n- Theme records carry a pre-computed ABGR-packed cache for every color slot; cache is filled when the theme is registered and refreshed defensively on every Switch()\n- HellionStyle.PushGlobal reads ABGR values from the cache instead of calling ColourUtil.RgbaToAbgr per slot per frame; ~13 % render-time recovery measured in typical scenes (plan estimate was 2–6 %, real ~10–15 %)\n- ThemeRegistry custom-theme reload distinguishes a recoverable file lock (editor mid-save) from a permanent IO failure; locked themes keep their last-known-good snapshot and retry on the next lookup instead of dropping out of the picker\n- New built-in: Synthwave Sunset — Hot Magenta + Cyan on midnight violet, 80s neon-grid vibes; tenth theme in the picker\n- Author credits refreshed: brand themes are credited as \"Hellion Forge\"; Mint Grove and Forge Merchantman now credited to Carla Beleandis as a community thanks\n\nNo schema bump, no user-visible behaviour change other than smoother frames on GC-sensitive setups and one additional colour option.\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.4.0 — Critical Lifecycle Fixes**\n\nFirst sub-patch of the v1.4.x Polish Sweep series. Seven known lifecycle and race bugs eliminated before any performance refactor sits on top.\n\n- MessageStore disposal no longer triggers GC.Collect globally; Pooling=false on the SQLite connection means there's nothing left to clean up by hand\n- PendingMessage and RetentionSweep worker threads are explicitly marked IsBackground=true so the plugin domain can unload during XIVLauncher reload without waiting for them\n- EmoteCache image and gif loaders moved from async-void to async Task with a shared task tracker, draining on Dispose so an in-flight load can no longer write to a disposed EmoteImages entry\n- DisposeAsync 10s timeout now warns loudly instead of silently leaving the worker behind\n- Plugin.Dispose flushes any pending DeferredSaveFrames before tearing services down, so settings changes made in the last few frames before disable are no longer lost\n- The v13→v14 config migration now reads the pre-v13 backup and carries HellionThemeWindowOpacity into the new WindowOpacity field instead of falling back to the default 0.85\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.3.0 - Plugin Integrations: Honorific**\n\nFirst step on the plugin-integration roadmap. HellionChat now listens to Honorific and shows your custom title in the chat header. The slot auto-hides when Honorific is not installed, when no custom title is active, or when you are using the original FFXIV title.\n\n- New \"Integrations\" settings tab\n- Honorific integration with auto-detection and live updates\n- \"Coming soon\" preview of the next five planned integrations: context menu actions, smart notifications, RP status block, ExtraChat channels, and quick DM compose\n- Maintainer attribution buttons for Honorific repo and Caraxi\n- New service-class pattern under HellionChat/Integrations/\n\nModding and support: join Hellion Forge - https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nEarlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, - "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.4.2/latest.zip", - "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.4.2/latest.zip", - "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.4.2/latest.zip", + "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.2/latest.zip", + "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.2/latest.zip", + "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.2/latest.zip", "TestingAssemblyVersion": "1.4.2.0", - "IconUrl": "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png", + "IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png", "ImageUrls": [ - "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png", - "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/settingsOverview.png", - "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/themesPicker.png" + "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png", + "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png", + "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png" ], "DownloadCount": 0, "IsHide": false, From 8dc8b87580f007b6f47beafcd03c3b3b8eaed67a Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 22:22:22 +0200 Subject: [PATCH 09/10] Bump version to 1.4.3 and sync manifest files --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 65 +++++++++++++++++++++++----------- README.md | 6 ++-- docs/CHANGELOG.md | 39 ++++++++++++++++++++ docs/ROADMAP.md | 30 +++++++++++++--- repo.json | 14 ++++---- 6 files changed, 120 insertions(+), 36 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index f158bb5..76c8de2 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.4.2 + 1.4.3 enable enable