From df3d5d78d68805a04f2dcaf7dd40b4fc4b716ed2 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 16:53:22 +0200 Subject: [PATCH 01/18] build(preflight): add csharpier and markdownlint blocks (G1) Block E runs 'dotnet csharpier check' against the HellionChat/ tree, catching reflow drift before push. Block F runs markdownlint-cli2 over the repo's *.md files; MD036 is disabled because forge-post bodies use bold emphasis as section headings (the auto-announce workflow renders those as Discord embeds, so the bold pattern is required). The .claude directory is excluded from the lint scope to match its gitignore status. .markdownlint.json also gains MD024 with siblings_only:true so per-release '### Internal' sub-headers in CHANGELOG.md don't trip the rule across sibling H2 sections. --- .markdownlint.json | 2 ++ scripts/preflight.sh | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.markdownlint.json b/.markdownlint.json index 8f989cf..085bab1 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,7 +1,9 @@ { "MD007": { "indent": 4 }, "MD013": false, + "MD024": { "siblings_only": true }, "MD029": false, "MD033": false, + "MD036": false, "MD041": false } diff --git a/scripts/preflight.sh b/scripts/preflight.sh index adc53bf..280858f 100755 --- a/scripts/preflight.sh +++ b/scripts/preflight.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # preflight.sh — pre-push gate. Blocks A/B/C verify config drift; Block D is a -# headless `dotnet build` to catch compile-time API drift. Test execution lives -# in the local Build-Suite repo and is NOT part of this preflight. +# headless `dotnet build` to catch compile-time API drift; Block E runs +# `dotnet csharpier check` against HellionChat/; Block F runs markdownlint +# against the repo's *.md files. Test execution lives in the local Build-Suite +# repo and is NOT part of this preflight. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -19,4 +21,12 @@ echo "==> preflight: Block C — changelog sync" echo "==> preflight: Block D — plugin compile health" dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet +echo "==> preflight: Block E — csharpier reflow check" +dotnet csharpier check HellionChat/ + +echo "==> preflight: Block F — markdownlint" +# npx --yes avoids a global install; first run caches into ~/.npm/_npx/. +# Subsequent runs are sub-second. +npx --yes markdownlint-cli2 "**/*.md" "#node_modules" "#bin" "#obj" "#.claude" + echo "==> preflight: ALL GREEN" From b9d3ff8f268bdf85fd4f3213d321ff5c3e5e21f3 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 17:19:28 +0200 Subject: [PATCH 02/18] fix(fonts): broaden font fallback catch to handle atlas-toolkit throws (G2) The atlas-toolkit pipeline can throw InvalidOperationException or ArgumentException when a configured font is structurally broken (e.g. unreadable header, unsupported glyph table). Previously only IO-shaped throws routed to the NotoSansCjkRegular fallback, so a corrupt font config would take down the entire atlas build instead of degrading gracefully. The warning log now carries the exception type name so the diagnostic path can tell which class of throw triggered the fallback. --- HellionChat/FontManager.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index 17daf5d..809846f 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -226,11 +226,21 @@ public class FontManager return fontId.AddToBuildToolkit(tk, config); } catch (Exception e) - when (e is FileNotFoundException or DirectoryNotFoundException or IOException) + when (e + is FileNotFoundException + or DirectoryNotFoundException + or IOException + or InvalidOperationException + or ArgumentException + ) { + // Atlas-toolkit throws span IO and validation failures; routing the + // wider set through the fallback keeps a corrupt font config from + // taking down the whole atlas build. Plugin.Log.Warning( e, - $"Configured {slot} font unavailable, falling back to NotoSansCjkRegular" + $"Configured {slot} font failed to load ({e.GetType().Name}), " + + "falling back to NotoSansCjkRegular" ); var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular); return fallback.AddToBuildToolkit(tk, config); From dd597fca4420d43fc11b01d761faad7cfb326718 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 17:48:51 +0200 Subject: [PATCH 03/18] feat(branding): validate URL constants on module init (F11.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BrandingLinks (5 Hellion-owned URLs) and IntegrationLinks (2 third-party plugin URLs) now run through UrlValidation.ValidateAll from a [ModuleInitializer] hook. A malformed URL throws InvalidOperationException at plugin load with the source class and the broken URL in the message, instead of silently failing when a user clicks the button. CA2255 is suppressed at the attribute sites — the warning is for library code shipped to unknown consumers, but the plugin DLL is loaded directly by Dalamud, which makes module-init the right one-shot hook. --- HellionChat/Branding/BrandingLinks.cs | 21 ++++++++++++++++++ HellionChat/Integrations/IntegrationLinks.cs | 12 ++++++++++ HellionChat/Util/UrlValidation.cs | 23 ++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 HellionChat/Util/UrlValidation.cs diff --git a/HellionChat/Branding/BrandingLinks.cs b/HellionChat/Branding/BrandingLinks.cs index c6eec3c..f3f3a08 100644 --- a/HellionChat/Branding/BrandingLinks.cs +++ b/HellionChat/Branding/BrandingLinks.cs @@ -1,3 +1,6 @@ +using System.Runtime.CompilerServices; +using HellionChat.Util; + namespace HellionChat.Branding; // Centralised — a future invite/URL rotation only touches this file. @@ -9,4 +12,22 @@ internal static class BrandingLinks "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat"; public const string HellionForgeWebsite = "https://hellion-forge.cloud"; public const string HellionMediaWebsite = "https://hellion-media.de/de"; + + // CA2255 warns against [ModuleInitializer] in library code, but Dalamud + // loads the plugin DLL directly so the module-init pass is the right hook + // for a one-shot URL sanity check at plugin load. +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void ValidateUrls() + { + UrlValidation.ValidateAll( + nameof(BrandingLinks), + HellionForgeDiscordInvite, + HellionForgeGitea, + HellionChatRepo, + HellionForgeWebsite, + HellionMediaWebsite + ); + } } diff --git a/HellionChat/Integrations/IntegrationLinks.cs b/HellionChat/Integrations/IntegrationLinks.cs index 84253a7..65bb5a6 100644 --- a/HellionChat/Integrations/IntegrationLinks.cs +++ b/HellionChat/Integrations/IntegrationLinks.cs @@ -1,3 +1,6 @@ +using System.Runtime.CompilerServices; +using HellionChat.Util; + namespace HellionChat.Integrations; // Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs). @@ -5,4 +8,13 @@ internal static class IntegrationLinks { public const string HonorificRepo = "https://github.com/Caraxi/Honorific"; public const string HonorificAuthor = "https://github.com/Caraxi"; + + // See BrandingLinks.ValidateUrls for the CA2255 rationale. +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void ValidateUrls() + { + UrlValidation.ValidateAll(nameof(IntegrationLinks), HonorificRepo, HonorificAuthor); + } } diff --git a/HellionChat/Util/UrlValidation.cs b/HellionChat/Util/UrlValidation.cs new file mode 100644 index 0000000..98d49b7 --- /dev/null +++ b/HellionChat/Util/UrlValidation.cs @@ -0,0 +1,23 @@ +namespace HellionChat.Util; + +internal static class UrlValidation +{ + // Used by BrandingLinks/IntegrationLinks at module init. A typo in a URL + // rotation throws loudly at plugin load instead of silently failing when + // a user clicks the broken button. + public static void ValidateAll(string source, params string[] urls) + { + foreach (var url in urls) + { + if ( + !Uri.TryCreate(url, UriKind.Absolute, out var uri) + || (uri.Scheme is not "https" and not "http") + ) + { + throw new InvalidOperationException( + $"{source} contains malformed URL: {url}" + ); + } + } + } +} From 28ea2fa5534ae7d5c1b8f6cc3b8c2c8a179b03c3 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 18:19:15 +0200 Subject: [PATCH 04/18] refactor(theme): extract ChildBgAlpha threshold logic to testable helper (F1.2) HellionStyle.PushGlobal had two lines that resolved the child-bg alpha based on window opacity. Moves the 0.999f threshold and the alpha-mask into HellionStyleHelpers.ResolveChildBgAlpha so the logic is reachable from the build suite without touching the ImGui surface. --- HellionChat/Ui/HellionStyle.cs | 11 ++++------- HellionChat/Ui/HellionStyleHelpers.cs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 HellionChat/Ui/HellionStyleHelpers.cs diff --git a/HellionChat/Ui/HellionStyle.cs b/HellionChat/Ui/HellionStyle.cs index 4d0774a..96a6b7f 100644 --- a/HellionChat/Ui/HellionStyle.cs +++ b/HellionChat/Ui/HellionStyle.cs @@ -43,13 +43,10 @@ internal static class HellionStyle var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF); var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte; - // ChildBg alpha: child areas rendered inside ChatLogWindow would - // multiply their alpha with WindowBg, making 50% opacity appear - // ~75% solid. At full opacity the theme's alpha is preserved; below - // it ChildBg goes fully transparent so only WindowBg sets the final - // coverage. - var childBgAlpha = windowOpacity >= 0.999f ? (c.ChildBg & 0xFFu) : 0u; - var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | childBgAlpha; + // ChildBg alpha resolution lives in HellionStyleHelpers so the + // threshold logic can be covered by a pure-helper test in the + // build suite. + var childBgWithAlpha = HellionStyleHelpers.ResolveChildBgAlpha(c.ChildBg, windowOpacity); // Layout stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding); diff --git a/HellionChat/Ui/HellionStyleHelpers.cs b/HellionChat/Ui/HellionStyleHelpers.cs new file mode 100644 index 0000000..d257681 --- /dev/null +++ b/HellionChat/Ui/HellionStyleHelpers.cs @@ -0,0 +1,17 @@ +namespace HellionChat.Ui; + +internal static class HellionStyleHelpers +{ + // Child surfaces are drawn over WindowBg, so at partial window opacity + // the theme's own ChildBg alpha would double-multiply and read too solid. + // Above ~full opacity we preserve the theme alpha; below it we wipe to 0 + // so WindowBg alone carries the coverage. The 0.999f threshold is a + // float-imprecision guard around the user-facing 100% slider value. + // TEST-MIRROR: ../../Hellion Build test/_Helpers/HellionStyleHelpersTests.cs + public static uint ResolveChildBgAlpha(uint themeChildBgRgba, float windowOpacity) + { + var alphaPreserved = windowOpacity >= 0.999f; + var childBgAlpha = alphaPreserved ? (themeChildBgRgba & 0xFFu) : 0u; + return (themeChildBgRgba & 0xFFFFFF00u) | childBgAlpha; + } +} From bdd64cad07583c525c5ffa7872425a6110e897c3 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 18:43:05 +0200 Subject: [PATCH 05/18] perf(ui): cache GetWindowDrawList per frame in SettingsOverview (F7.3) DrawCard used to call ImGui.GetWindowDrawList once per card, so a frame with 10 settings cards took 10 draw-list lookups. The list is the same for every card in the same frame, so Draw() now resolves it once and passes the pointer down. Pattern parity with ChatLogWindow's frame-local draw-list handling. --- HellionChat/Ui/SettingsOverview.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index 29b6eb8..be64b06 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -79,11 +79,13 @@ internal sealed class SettingsOverview // 110f accommodates two-line subtexts; wrap width is matched in DrawCard. var cardHeight = 110f; + // One draw-list lookup per frame instead of one per card. + var drawList = ImGui.GetWindowDrawList(); var cardDefs = BuildCardDefs(); for (var i = 0; i < cardDefs.Length; i++) { var (icon, title, subtext) = cardDefs[i]; - DrawCard(i, icon, title, subtext, cardWidth, cardHeight); + DrawCard(i, icon, title, subtext, cardWidth, cardHeight, drawList); if ((i + 1) % columns != 0 && i != cardDefs.Length - 1) ImGui.SameLine(); @@ -96,7 +98,8 @@ internal sealed class SettingsOverview string title, string subtext, float w, - float h + float h, + ImDrawListPtr drawList ) { // BeginGroup makes the card a single layout item so SameLine works @@ -108,8 +111,7 @@ internal sealed class SettingsOverview var hovered = ImGui.IsItemHovered(); var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u; - var draw = ImGui.GetWindowDrawList(); - draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); + drawList.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); var iconPos = cursorBefore + new Vector2(16f, 12f); var titlePos = cursorBefore + new Vector2(16f, 40f); @@ -120,15 +122,15 @@ internal sealed class SettingsOverview using (_window.Plugin.FontManager.FontAwesome.Push()) { - draw.AddText(iconPos, titleColor, icon.ToIconString()); + drawList.AddText(iconPos, titleColor, icon.ToIconString()); } - draw.AddText(titlePos, titleColor, title); + drawList.AddText(titlePos, titleColor, title); // Subtext wraps at card inner width (16px padding each side) via DrawList // to avoid expanding the group bounds and breaking SameLine in the card row. var subtextWrapWidth = w - 32f; - draw.AddText( + drawList.AddText( ImGui.GetFont(), ImGui.GetFontSize(), subtextPos, From 2684c31f10712d0bce02ed13885ce3986b741584 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 19:09:43 +0200 Subject: [PATCH 06/18] fix(ui): scale active-tab underline with DPI for crisp rendering (F7.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2px underline pill was hardcoded — at 125/150% DPI the surrounding tab layout scaled with ImGuiHelpers.GlobalScale but the pill stayed 2px, so the line landed on sub-pixel boundaries and rendered as a fuzzy band. Now: height scales with GlobalScale (clamped to >=1px), and the DrawList coordinates round to physical pixels via MathF.Round so the rect aligns with the framebuffer grid. --- HellionChat/Ui/ChatLogWindow.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 0e90f83..9bb1f79 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1637,17 +1637,21 @@ public sealed class ChatLogWindow : Window continue; // Active-tab underline pill (2px accent). No native ImGui underline API, - // so we use a direct DrawList pass. + // so we use a direct DrawList pass. Pill height scales with GlobalScale + // and all coordinates round to physical pixels so the line stays crisp + // on 125/150% DPI setups instead of bleeding into a sub-pixel blur. { var theme = Plugin.ThemeRegistry.Active; var min = ImGui.GetItemRectMin(); var max = ImGui.GetItemRectMax(); - const float pillHeight = 2f; + var pillHeight = MathF.Max(1f, MathF.Round(2f * ImGuiHelpers.GlobalScale)); + var yBottom = MathF.Round(max.Y); + var yTop = yBottom - pillHeight; ImGui .GetWindowDrawList() .AddRectFilled( - new Vector2(min.X, max.Y - pillHeight), - new Vector2(max.X, max.Y), + new Vector2(MathF.Round(min.X), yTop), + new Vector2(MathF.Round(max.X), yBottom), ColourUtil.RgbaToAbgr(theme.Colors.Accent) ); } From ae1436b103ed49f27b2cc49f131d0bbdb90784f7 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 19:35:17 +0200 Subject: [PATCH 07/18] perf(config): clone only temp tabs in SaveConfig snapshot/restore (F2.2) The pre-serialization snapshot used to clone the entire Config.Tabs list, then Clear/AddRange the snapshot back. With a typical config of ~30 user-defined tabs plus up to 15 session-only temp tabs, that's a 45-item clone on every save. The persistent tabs never leave the list during this routine, so cloning only the temp subset is functionally identical and keeps the allocation proportional to AutoTellTabsLimit. --- HellionChat/Plugin.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index e90b135..3d49324 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -637,18 +637,20 @@ public sealed class Plugin : IAsyncDalamudPlugin internal void SaveConfig() { - // Strip session-only Auto-Tell-Tabs before serialization; restore after. - var snapshot = Config.Tabs.ToList(); + // Session-only Auto-Tell-Tabs aren't persisted, so they move aside + // before serialization and re-attach after. Cloning only the temp + // subset keeps the allocation proportional to AutoTellTabsLimit + // (<=15) instead of the full tab list. + var tempTabs = Config.Tabs.Where(t => t.IsTempTab).ToList(); Config.Tabs.RemoveAll(t => t.IsTempTab); Interface.SavePluginConfig(Config); - Config.Tabs.Clear(); - Config.Tabs.AddRange(snapshot); + Config.Tabs.AddRange(tempTabs); - // F2.1: snapshot-restore preserves IsTempTab tabs but the mid-step - // RemoveAll bypasses AutoTellTabsService, so re-peg the counter. - // Null-conditional because SaveConfig can fire before Phase-2 init. + // F2.1: the mid-step RemoveAll bypasses AutoTellTabsService, so + // re-peg the counter. Null-conditional because SaveConfig can fire + // before Phase-2 init. AutoTellTabsService?.ResyncTempTabCounter(); } From 6b44f549b41cd3e7ed9c59d3ce19e969686050d4 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:10:40 +0200 Subject: [PATCH 08/18] feat(util): add IPlatformUtil indirection over Dalamud.Utility.Util (F12.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a thin interface around Util.IsWine and Util.OpenLink so services can be constructed in an isolated xUnit AppDomain without forcing Dalamud.dll onto the assembly search path. Production wiring (DalamudPlatformUtil) caches IsWine at ctor time — it's a runtime probe that never changes for the lifetime of a plugin instance, mirroring the Lightless DalamudUtilService pattern. Plugin.PlatformUtil is wired in the Phase-1 ctor so any service that LoadAsync allocates can resolve the platform indirection without plumbing the instance through additional constructor params. Follow-up commits route MessageStore and the OpenLink call-sites through this interface. --- HellionChat/Plugin.cs | 9 +++++++++ HellionChat/Util/DalamudPlatformUtil.cs | 16 ++++++++++++++++ HellionChat/Util/IPlatformUtil.cs | 11 +++++++++++ 3 files changed, 36 insertions(+) create mode 100644 HellionChat/Util/DalamudPlatformUtil.cs create mode 100644 HellionChat/Util/IPlatformUtil.cs diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 3d49324..717e3fc 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -113,6 +113,10 @@ public sealed class Plugin : IAsyncDalamudPlugin internal Ui.StatusBar StatusBar { get; private set; } = null!; internal Integrations.HonorificService HonorificService { get; private set; } = null!; + // Platform indirection over Dalamud.Utility.Util. Wired in Phase-1 ctor so + // any service allocated in LoadAsync can read Plugin.PlatformUtil. + internal static IPlatformUtil PlatformUtil { get; private set; } = null!; + // Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race. private int _disposeStarted; @@ -154,6 +158,11 @@ public sealed class Plugin : IAsyncDalamudPlugin Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); + // Wire platform indirection before LoadAsync allocates anything that + // needs Util.* — services then read Plugin.PlatformUtil instead of + // hitting the Dalamud static surface directly. + PlatformUtil = new DalamudPlatformUtil(); + // Schema gate: v1.4.x requires config v16. Users on older schemas // must install v1.4.2 first to run the migration chain. if (Config.Version < 16) diff --git a/HellionChat/Util/DalamudPlatformUtil.cs b/HellionChat/Util/DalamudPlatformUtil.cs new file mode 100644 index 0000000..fbed84b --- /dev/null +++ b/HellionChat/Util/DalamudPlatformUtil.cs @@ -0,0 +1,16 @@ +namespace HellionChat.Util; + +internal sealed class DalamudPlatformUtil : IPlatformUtil +{ + public DalamudPlatformUtil() + { + // Util.IsWine probes the host process and never changes for the + // lifetime of a plugin instance, so we cache it once at ctor. + // Mirrors LightlessSync/Services/DalamudUtilService:154. + IsWine = Dalamud.Utility.Util.IsWine(); + } + + public bool IsWine { get; } + + public void OpenLink(string url) => Dalamud.Utility.Util.OpenLink(url); +} diff --git a/HellionChat/Util/IPlatformUtil.cs b/HellionChat/Util/IPlatformUtil.cs new file mode 100644 index 0000000..6fadc03 --- /dev/null +++ b/HellionChat/Util/IPlatformUtil.cs @@ -0,0 +1,11 @@ +namespace HellionChat.Util; + +// Indirection over Dalamud.Utility.Util's static surface so services can be +// constructed in an isolated xUnit AppDomain without loading Dalamud.dll. +// Production wiring lives in DalamudPlatformUtil; tests substitute a fake. +internal interface IPlatformUtil +{ + bool IsWine { get; } + + void OpenLink(string url); +} From 4510c1e40486a99c9bada898662ecb4a37b8bd18 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:29:22 +0200 Subject: [PATCH 09/18] refactor(store): route MessageStore IsWine probe through IPlatformUtil (F12.1) MessageStore.Connect used to call Util.IsWine() directly via a DalamudUtil alias, which made the ctor unreachable from the xUnit test AppDomain: any test that allocated a MessageStore tripped a FileNotFoundException on Dalamud.dll before reaching the assertion. The ctor now takes an IPlatformUtil and reads the cached IsWine property. MessageManager passes Plugin.PlatformUtil in. Production behaviour is identical; the test path can now substitute a fake and exercise the SQLite migration logic in isolation. --- HellionChat/MessageManager.cs | 2 +- HellionChat/MessageStore.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index 7d74ede..d50000d 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -52,7 +52,7 @@ internal class MessageManager : IAsyncDisposable { Plugin = plugin; - Store = new MessageStore(DatabasePath()); + Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil); PendingMessageThread = new Thread(() => ProcessPendingMessages(PendingThreadCancellationToken.Token) diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs index ae4f213..b200c5f 100644 --- a/HellionChat/MessageStore.cs +++ b/HellionChat/MessageStore.cs @@ -9,7 +9,6 @@ using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; using Microsoft.Data.Sqlite; -using DalamudUtil = Dalamud.Utility.Util; using Encoding = System.Text.Encoding; namespace HellionChat; @@ -137,9 +136,12 @@ internal class MessageStore : IDisposable ) ); - internal MessageStore(string dbPath) + private readonly IPlatformUtil _platformUtil; + + internal MessageStore(string dbPath, IPlatformUtil platformUtil) { DbPath = dbPath; + _platformUtil = platformUtil; Connection = Connect(); Migrate(); } @@ -166,7 +168,7 @@ internal class MessageStore : IDisposable conn.Open(); conn.Execute(@"PRAGMA journal_mode=WAL;"); conn.Execute(@"PRAGMA synchronous=NORMAL;"); - if (DalamudUtil.IsWine()) + if (_platformUtil.IsWine) conn.Execute(@"PRAGMA cache_size = 32768;"); return conn; } From 28e4b30cd679a8918a596e0e2534aecf5febdb14 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:32:17 +0200 Subject: [PATCH 10/18] refactor(ui): route OpenLink call-sites through Plugin.PlatformUtil (F12.1) Ten Util.OpenLink call-sites across five files now go through the IPlatformUtil indirection: WrapperUtil.TryOpenUri, the Settings Ko-Fi buttons (x2), the Information tab (issues link plus media/upstream links, x3), the Integrations tab (Honorific repo/author plus forge discord, x3), and the ThemeAndLayout 'open themes folder' button. A future addition to this pattern only needs to plug into IPlatformUtil instead of touching Dalamud.Utility.Util directly. --- HellionChat/Ui/Settings.cs | 4 ++-- HellionChat/Ui/SettingsTabs/Information.cs | 6 +++--- HellionChat/Ui/SettingsTabs/Integrations.cs | 6 +++--- HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs | 2 +- HellionChat/Util/WrapperUtil.cs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index a6d93a3..689249c 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -199,12 +199,12 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window ); if (ImGui.Button(buttonLabel2)) - Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii"); + Plugin.PlatformUtil.OpenLink("https://ko-fi.com/infiii"); ImGui.SameLine(); if (ImGui.Button(buttonLabel)) - Dalamud.Utility.Util.OpenLink("https://ko-fi.com/lojewalo"); + Plugin.PlatformUtil.OpenLink("https://ko-fi.com/lojewalo"); } if (!save) diff --git a/HellionChat/Ui/SettingsTabs/Information.cs b/HellionChat/Ui/SettingsTabs/Information.cs index 2c9d52f..09428d1 100644 --- a/HellionChat/Ui/SettingsTabs/Information.cs +++ b/HellionChat/Ui/SettingsTabs/Information.cs @@ -97,7 +97,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( + Plugin.PlatformUtil.OpenLink( "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues" ); } @@ -116,7 +116,7 @@ internal sealed class Information : ISettingsTab ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label); ImGui.SameLine(); if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia")) - Dalamud.Utility.Util.OpenLink("https://hellion-media.de"); + Plugin.PlatformUtil.OpenLink("https://hellion-media.de"); ImGuiHelpers.ScaledDummy(10.0f); @@ -137,7 +137,7 @@ internal sealed class Information : ISettingsTab ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label); ImGui.SameLine(); if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream")) - Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo"); + Plugin.PlatformUtil.OpenLink("https://github.com/Infiziert90/ChatTwo"); ImGuiHelpers.ScaledDummy(10.0f); diff --git a/HellionChat/Ui/SettingsTabs/Integrations.cs b/HellionChat/Ui/SettingsTabs/Integrations.cs index dba5bfb..b422764 100644 --- a/HellionChat/Ui/SettingsTabs/Integrations.cs +++ b/HellionChat/Ui/SettingsTabs/Integrations.cs @@ -79,12 +79,12 @@ internal sealed class Integrations : ISettingsTab ImGui.Spacing(); if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo)) { - Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificRepo); + Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificRepo); } ImGui.SameLine(); if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkAuthor)) { - Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificAuthor); + Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificAuthor); } } @@ -193,7 +193,7 @@ internal sealed class Integrations : ISettingsTab if (ImGui.Button(HellionStrings.Settings_Integrations_GotAnIdea_LinkLabel)) { - Dalamud.Utility.Util.OpenLink(BrandingLinks.HellionForgeDiscordInvite); + Plugin.PlatformUtil.OpenLink(BrandingLinks.HellionForgeDiscordInvite); } } diff --git a/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs b/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs index 5e720b9..d87275d 100644 --- a/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs +++ b/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs @@ -78,7 +78,7 @@ internal sealed class ThemeAndLayout : ISettingsTab { var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes"); Directory.CreateDirectory(dir); - Dalamud.Utility.Util.OpenLink(dir); + Plugin.PlatformUtil.OpenLink(dir); } ImGui.SameLine(); diff --git a/HellionChat/Util/WrapperUtil.cs b/HellionChat/Util/WrapperUtil.cs index 0edd742..4a4270c 100644 --- a/HellionChat/Util/WrapperUtil.cs +++ b/HellionChat/Util/WrapperUtil.cs @@ -22,7 +22,7 @@ public static class WrapperUtil try { Plugin.Log.Debug($"Opening URI {uri} in default browser"); - Dalamud.Utility.Util.OpenLink(uri.ToString()); + Plugin.PlatformUtil.OpenLink(uri.ToString()); } catch (Exception ex) { From f8b5c14509a10865ae871206ad72f00b0f244993 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:38:12 +0200 Subject: [PATCH 11/18] fix(config): deep-clone UsedChannel and TellTarget in Tab.Clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12). Tab.Clone() used to assign CurrentChannel = CurrentChannel and run TellTarget.From(TellTarget). The first was a plain reference copy of the UsedChannel — the clone and the source shared the same channel state, so a channel switch or TellTarget update on a PopOut/Temp tab also mutated its origin tab. The second was a static factory call that read like a constructor where every other place uses Clone(). - TellTarget: static From(t) replaced by instance Clone(); only call-site swapped to TellTarget.Clone(). - UsedChannel: new Clone() that copies the scalar fields and runs Clone() on the two TellTarget references (null-safe). - Tab.Clone(): CurrentChannel goes through UsedChannel.Clone(). --- HellionChat/Configuration.cs | 27 +++++++++++++++++-- HellionChat/GameFunctions/Types/TellTarget.cs | 8 +++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index c463467..fee30ca 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -500,7 +500,7 @@ public class Tab Opacity = Opacity, Identifier = Identifier, InputDisabled = InputDisabled, - CurrentChannel = CurrentChannel, + CurrentChannel = CurrentChannel.Clone(), CanMove = CanMove, CanResize = CanResize, IndependentHide = IndependentHide, @@ -512,7 +512,7 @@ public class Tab HideWhenInactive = HideWhenInactive, IsTempTab = IsTempTab, AllSenderMessages = AllSenderMessages, - TellTarget = TellTarget.From(TellTarget), + TellTarget = TellTarget.Clone(), IsGreeted = IsGreeted, }; } @@ -690,6 +690,29 @@ public class UsedChannel { Channel = channel; } + + // --------------------------------------------------------------- + // Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12) + // - Deep-clone the UsedChannel so Tab.Clone() no longer shares + // channel state (incl. TellTarget) with its origin Tab. Previously + // a reference copy: PopOut and Temp tabs mutated each other. + // - Name is intentionally a reference copy (matches upstream); it + // gets reassigned on every channel switch anyway. + // TEST-MIRROR: ../../Hellion Build test/_Helpers/UsedChannelCloneTests.cs + // --------------------------------------------------------------- + public UsedChannel Clone() + { + return new UsedChannel + { + Channel = Channel, + Name = Name, + TellTarget = TellTarget?.Clone(), + + UseTempChannel = UseTempChannel, + TempChannel = TempChannel, + TempTellTarget = TempTellTarget?.Clone(), + }; + } } [Serializable] diff --git a/HellionChat/GameFunctions/Types/TellTarget.cs b/HellionChat/GameFunctions/Types/TellTarget.cs index e792e24..e1654e4 100755 --- a/HellionChat/GameFunctions/Types/TellTarget.cs +++ b/HellionChat/GameFunctions/Types/TellTarget.cs @@ -40,5 +40,11 @@ public class TellTarget public static TellTarget Empty() => new(string.Empty, 0, 0, TellReason.Direct); - public static TellTarget From(TellTarget t) => new(t.Name, t.World, t.ContentId, t.Reason); + // --------------------------------------------------------------- + // Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12) + // - Replaced static From(t) with an instance-style Clone() so call + // sites read like a copy operation, not a factory. + // TEST-MIRROR: ../../../Hellion Build test/_Helpers/TellTargetCloneTests.cs + // --------------------------------------------------------------- + public TellTarget Clone() => new(Name, World, ContentId, Reason); } From db48f2784242662a99bd6979b86b8df9e7fe76f3 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:39:23 +0200 Subject: [PATCH 12/18] fix(chat): release Utf8String when linkshell check rejects channel Cherry-pick from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12). Chat.SetChannel allocates a native Utf8String for the target name and then runs a validity check. The previous early return on an invalid linkshell skipped Dtor and leaked the native allocation; every invalid linkshell switch added one Utf8String to the unmanaged heap. - Renamed ValidAnyLinkshell to IsChannelOrExistingLinkshell so the call-site reads naturally. - Wrapped ChangeChatChannel in the validity check instead of early-returning. Dtor now runs on every path. - ChatLogWindow follows the rename at its single call-site. --- HellionChat/GameFunctions/Chat.cs | 35 +++++++++++++++++++++---------- HellionChat/Ui/ChatLogWindow.cs | 5 ++++- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/HellionChat/GameFunctions/Chat.cs b/HellionChat/GameFunctions/Chat.cs index b4a0396..4d75d99 100755 --- a/HellionChat/GameFunctions/Chat.cs +++ b/HellionChat/GameFunctions/Chat.cs @@ -423,16 +423,24 @@ internal sealed unsafe class Chat : IDisposable ); } - // Check if channel is valid (non-linkshell or existing linkshell) - internal static bool ValidAnyLinkshell(InputChannel channel) + // --------------------------------------------------------------- + // Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12) + // - Renamed ValidAnyLinkshell -> IsChannelOrExistingLinkshell. The + // name now states intent: returns true for any non-linkshell + // channel, or a linkshell index that actually exists. + // --------------------------------------------------------------- + internal static bool IsChannelOrExistingLinkshell(InputChannel channel) { var idx = channel.LinkshellIndex(); if (idx == uint.MaxValue || channel.IsExtraChatLinkshell()) return true; - if (channel.IsLinkshell() && ValidLinkshell(idx)) - return true; - if (channel.IsCrossLinkshell() && ValidCrossLinkshell(idx)) - return true; + + if (channel.IsLinkshell()) + return ValidLinkshell(idx); + + if (channel.IsCrossLinkshell()) + return ValidCrossLinkshell(idx); + return false; } @@ -531,12 +539,17 @@ internal sealed unsafe class Chat : IDisposable if (idx == uint.MaxValue) idx = 0; - if (!ValidAnyLinkshell(channel)) - return; + // --------------------------------------------------------------- + // Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12) + // - Wrap ChangeChatChannel in the validity check instead of + // early-returning. The previous early return skipped Dtor and + // leaked the native Utf8String allocated a few lines above. + // --------------------------------------------------------------- + if (IsChannelOrExistingLinkshell(channel)) + RaptureShellModule + .Instance() + ->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true); - RaptureShellModule - .Instance() - ->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true); target->Dtor(true); } diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 9bb1f79..0f43eda 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -272,7 +272,10 @@ public sealed class ChatLogWindow : Window } } - if (targetChannel == null || !GameFunctions.Chat.ValidAnyLinkshell(targetChannel.Value)) + if ( + targetChannel == null + || !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value) + ) { Plugin.Log.Warning( $"Channel was set to an invalid value '{targetChannel}', ignoring" From 7ac1eb3fd47fd9533eff5a6b5cbd5c21470b4eb1 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:42:17 +0200 Subject: [PATCH 13/18] fix(ui): pass measured width straight through IconButton, drop broken subtract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspired by ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12). Upstream dropped the width parameter entirely because nothing called it. We keep the parameter — two ChatLogWindow header buttons (Cog, EyeSlash) size themselves to match the preceding ChannelIcon button. The actual bug is local: the previous size = width - 2 * CellPadding.X mixed a raw int (HUD-scale unaware) with CellPadding.X (HUD-scaled), so the button shrank under elevated HUD scale. ImGui.Button handles its own frame padding internally, so the measured width passes through unchanged. --- HellionChat/Util/ImGuiUtil.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/HellionChat/Util/ImGuiUtil.cs b/HellionChat/Util/ImGuiUtil.cs index 96cd830..4db4141 100755 --- a/HellionChat/Util/ImGuiUtil.cs +++ b/HellionChat/Util/ImGuiUtil.cs @@ -254,6 +254,17 @@ internal static class ImGuiUtil return end; } + // --------------------------------------------------------------- + // Inspired by ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12). + // Upstream dropped the width parameter (no callers there); we keep + // it because two ChatLogWindow header buttons size themselves to + // match the ChannelIcon button's frame. The actual bug is the + // manual size = width - 2 * CellPadding.X subtraction: CellPadding + // scales with HUD scale, the raw int does not, so the button + // shrank under high HUD scales. ImGui.Button already handles its + // own frame padding internally — pass the measured width straight + // through. + // --------------------------------------------------------------- internal static bool IconButton( FontAwesomeIcon icon, string? id = null, @@ -268,10 +279,7 @@ internal static class ImGuiUtil bool ret; using (Plugin.FontManager.FontAwesome.Push()) { - var size = Vector2.Zero; - if (width > 0) - size.X = width - 2 * ImGui.GetStyle().CellPadding.X; - + var size = width > 0 ? new Vector2(width, 0f) : Vector2.Zero; ret = ImGui.Button(label, size); } From 5b972238bb3d1c71d5bf0f66e4167cc32c65a884 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:58:52 +0200 Subject: [PATCH 14/18] chore: bump version to 1.4.6 csproj , yaml changelog block (v1.4.6 added on top, v1.4.2 rotated out per the slim-4-versions rule), repo.json AssemblyVersion + TestingAssemblyVersion + the three DownloadLink URLs + Changelog string, all in sync. --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 46 ++++++++++++++++++++++++++-------- repo.json | 12 ++++----- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 3c23b26..43c63fa 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -1,7 +1,7 @@ - 1.4.5 + 1.4.6 enable enable diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index 4868efb..db7db94 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -35,6 +35,41 @@ tags: - Replacement - Privacy changelog: |- + **v1.4.6 — Code Hygiene and Refactor (2026-05-12)** + + Maintenance patch. No user-visible behaviour changes; tightens the + development feedback loop, fixes two upstream-inherited bugs, and + prepares the code for the v1.4.7 backlog cleanup. + + - preflight.sh gains a csharpier reflow check and a markdownlint + pass so style drift and markdown violations are caught at the + pre-push gate + - FontManager fallback catches the full set of atlas-toolkit + throws (IO, InvalidOperation, ArgumentException) — a corrupt + font config no longer takes down the whole atlas build + - BrandingLinks and IntegrationLinks URLs validated on plugin + load — a typo in a future URL rotation now throws at startup + - Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel + no longer leaks the native Utf8String when the linkshell check + rejects the channel + - Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now + deep-clones UsedChannel and TellTarget — PopOut and Temp tabs + no longer mutate each other's channel state + - Active-tab underline scales with DPI and rounds to physical + pixels for crisp rendering above 100% scaling + - IconButton width parameter no longer subtracts HUD-scaled + padding from a raw int (measured width passes through verbatim) + - Internal: HellionStyle ChildBgAlpha extracted to a testable + helper; Plugin.SaveConfig clones only the temp tabs; + SettingsOverview caches the draw-list per frame; + Dalamud.Utility.Util surface routed through an IPlatformUtil + indirection (MessageStore IsWine probe is now testable in + isolation) + + Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). + + --- + **v1.4.5 — UX and Robustness (2026-05-12)** Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw @@ -93,15 +128,4 @@ changelog: |- --- - **v1.4.2 — Smoother frames in the chat log** - - Per-frame allocations in the chat-log render path eliminated. - 2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups. - - - Card-mode: theme/border invariants hoisted out of the per-message loop - - Auto-tell tab tint and icon cached per tab - - Status bar aggregation runs on ~1% of frames instead of every frame - - --- - Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases diff --git a/repo.json b/repo.json index db924bb..6910163 100644 --- a/repo.json +++ b/repo.json @@ -3,7 +3,7 @@ "Author": "Jon Kazama (Hellion Forge)", "Name": "Hellion Chat", "InternalName": "HellionChat", - "AssemblyVersion": "1.4.5.0", + "AssemblyVersion": "1.4.6.0", "Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR", "ApplicableVersion": "any", "RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat", @@ -14,12 +14,12 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.", - "Changelog": "**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw failures surface as a notification, the first-run wizard has an explicit Later option, the input history clears on plugin reload, and the status bar version slot stops clipping in narrow windows.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- First-run wizard: explicit \"Later — keep defaults\" button. Closing the X no longer silently accepts the defaults; the wizard reopens on the next plugin load if nothing was picked\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system font if the embedded Hellion font resource is missing — logs a Warning\n\n---\n\n**v1.4.4 — Threading and IPC safety polish (2026-05-12)**\n\nFifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock falls away, and the privacy filter speaks up when an unknown ChatType shows up.\n\n- AutoTellTabs hot-path getter uses an Interlocked counter instead of taking the lock on every read\n- Honorific integration: per-method threading banners, plus Warning-level log on unsubscribe failure\n- AutoTranslate warmup thread marked IsBackground so plugin unload doesn't wait for it\n- PrivacyFilter logs once per unknown ChatType so a future patch's added channel doesn't drop off the radar\n- New installs persist unknown channels by default; existing configs keep their explicit choice\n\n---\n\n**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy startup work (migrations, hooks, windows) now runs async so Dalamud's UI stays responsive during load. Load time is comparable to v1.4.2 — this is the foundation for v1.4.4 optimisations.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\n**v1.4.2 — Smoother frames in the chat log**\n\nPer-frame allocations in the chat-log render path eliminated. 2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.\n\n- Card-mode: theme/border invariants hoisted out of the per-message loop\n- Auto-tell tab tint and icon cached per tab\n- Status bar aggregation runs on ~1% of frames instead of every frame\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases", + "Changelog": "**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two upstream-inherited bugs, and prepares the code for the v1.4.7 backlog cleanup.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- FontManager fallback catches the full set of atlas-toolkit throws (IO, InvalidOperation, ArgumentException) — a corrupt font config no longer takes down the whole atlas build\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now deep-clones UsedChannel and TellTarget — PopOut and Temp tabs no longer mutate each other's channel state\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- Internal: HellionStyle ChildBgAlpha extracted to a testable helper; Plugin.SaveConfig clones only the temp tabs; SettingsOverview caches the draw-list per frame; Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation)\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw failures surface as a notification, the first-run wizard has an explicit Later option, the input history clears on plugin reload, and the status bar version slot stops clipping in narrow windows.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- First-run wizard: explicit \"Later — keep defaults\" button. Closing the X no longer silently accepts the defaults; the wizard reopens on the next plugin load if nothing was picked\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system font if the embedded Hellion font resource is missing — logs a Warning\n\n---\n\n**v1.4.4 — Threading and IPC safety polish (2026-05-12)**\n\nFifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock falls away, and the privacy filter speaks up when an unknown ChatType shows up.\n\n- AutoTellTabs hot-path getter uses an Interlocked counter instead of taking the lock on every read\n- Honorific integration: per-method threading banners, plus Warning-level log on unsubscribe failure\n- AutoTranslate warmup thread marked IsBackground so plugin unload doesn't wait for it\n- PrivacyFilter logs once per unknown ChatType so a future patch's added channel doesn't drop off the radar\n- New installs persist unknown channels by default; existing configs keep their explicit choice\n\n---\n\n**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy startup work (migrations, hooks, windows) now runs async so Dalamud's UI stays responsive during load. Load time is comparable to v1.4.2 — this is the foundation for v1.4.4 optimisations.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, - "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.5/latest.zip", - "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.5/latest.zip", - "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.5/latest.zip", - "TestingAssemblyVersion": "1.4.5.0", + "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip", + "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip", + "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip", + "TestingAssemblyVersion": "1.4.6.0", "IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png", "ImageUrls": [ "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png", From 0d016aaa5da72af8078ced3a80c6c6bf514ac8ff Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:58:57 +0200 Subject: [PATCH 15/18] docs: log v1.4.6 release notes CHANGELOG.md gets the full per-bullet block, ROADMAP.md gets the released-cycle summary plus a v1.4.7 next-cycle placeholder, README status section and version badge updated. --- README.md | 30 ++++++++++++++++-------------- docs/CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ docs/ROADMAP.md | 27 +++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5c8be77..7ddf9cc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![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/badge/release-v1.4.5-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest) +[![Latest release](https://img.shields.io/badge/release-v1.4.6-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/) @@ -11,7 +11,7 @@ Hellion Forge

-**Version 1.4.5** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on +**Version 1.4.6** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2 @@ -286,18 +286,20 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo ## Project Status -**Version 1.4.5** — User-visible robustness polish on top of the v1.4.4 threading work. The chat log no longer fails -silently: a draw-path exception now triggers a one-shot warning notification that points users at `/xllog`, while the -stack trace itself keeps going through `Plugin.Log.Error` as before. The first-run wizard splits accept from close — -`OnClose` no longer silently sets `FirstRunCompleted`, so closing the X leaves the wizard pending and it reopens on the -next plugin load; a new footer "Later — keep defaults" button is the explicit path to dismiss without picking a profile. -`InputHistoryService` clears on plugin dispose alongside the existing pure-memory cleanups, so the previous session's -typed commands don't bleed into the next load. `FontManager.GetHellionFontBytes` becomes a `Try`-variant that falls back -to the system-font path when the embedded resource is missing (broken csproj / dev build) instead of throwing through -the UiBuilder. The status bar drops the right-aligned version slot when the chat window is below the threshold needed to -fit all five slots without overlap. Internal: explicit session-only Auto-Tell-Tab invariant comment with a -`TempTabCounter.InitFromList` pin in the Build-Suite. No schema bump, no migration. Sixth sub-patch of the v1.4.x polish -sweep series (as of 2026-05-12). +**Version 1.4.6** — Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop and +pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier reflow check and a markdownlint pass at +the pre-push gate. `FontManager`'s font-fallback catch-filter now covers `InvalidOperationException` and +`ArgumentException` on top of the IO triad, so a corrupted font config no longer takes down the atlas build. +`BrandingLinks` and `IntegrationLinks` URLs validate themselves on plugin load — a typo in a future URL rotation throws +at startup instead of failing silently when a user clicks the broken button. Cherry-picked from ChatTwo upstream +`f35b7d3`: `Chat.SetChannel` no longer leaks the native `Utf8String` when the linkshell check rejects the channel, and +`Tab.Clone` now deep-clones `UsedChannel` and `TellTarget` (the previous reference copy let PopOut and Temp tabs mutate +each other's channel state). The active-tab underline pill scales with DPI and rounds to physical pixels for crisp +rendering above 100 % DPI. Internal items: `HellionStyle` ChildBgAlpha extracted to a testable helper, +`Plugin.SaveConfig` clones only the temp-tab subset, `SettingsOverview` caches the draw-list per frame, +`Dalamud.Utility.Util` static surface routed through an `IPlatformUtil` indirection (`MessageStore`'s `IsWine` probe is +now testable in isolation). No schema bump, no migration. Seventh sub-patch of the v1.4.x polish sweep series (as of +2026-05-12). Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed: diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 20e8903..b4b70ea 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,50 @@ to the release pages for details. --- +## Hellion Chat 1.4.6 — Code Hygiene and Refactor (2026-05-12) + +Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two +upstream-inherited bugs from ChatTwo `f35b7d3`, and prepares the code for the v1.4.7 backlog cleanup. + +- `scripts/preflight.sh` gains Block E (`dotnet csharpier check`) and Block F (`markdownlint-cli2`) so reflow drift and + markdown violations are caught at the pre-push gate. `.markdownlint.json` adds `MD024 siblings_only` and disables + `MD036` so the bilingual forge-post bold-emphasis headings pass linting; the `.claude/` directory is excluded from the + scan +- `FontManager.AddFontWithFallback` catch-filter now covers `InvalidOperationException` and `ArgumentException` on top + of the existing IO triad. The warning log carries the exception type name, so the diagnostic path knows which class + of atlas-toolkit throw triggered the NotoSansCjkRegular fallback +- `BrandingLinks` (5 URLs) and `Integrations/IntegrationLinks` (2 URLs) validate themselves on first module load via + `[ModuleInitializer]` + a shared `UrlValidation.ValidateAll` helper. A malformed URL now throws + `InvalidOperationException` at plugin load with the source class and the broken URL in the message +- Cherry-picked from ChatTwo upstream `f35b7d3`: `Chat.SetChannel` no longer leaks the native `Utf8String` when the + linkshell check rejects the channel. The validity check is now wrapped around the `ChangeChatChannel` call instead of + short-circuiting before `Dtor`. `ValidAnyLinkshell` is renamed to `IsChannelOrExistingLinkshell` and the + `ChatLogWindow` call-site follows the rename +- Cherry-picked from ChatTwo upstream `f35b7d3`: `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget`. The old + `CurrentChannel = CurrentChannel` was a reference copy, so PopOut and Temp tabs mutated each other's channel state + (incl. tell target). `TellTarget.From(t)` static factory is replaced with an instance `Clone()`; `UsedChannel.Clone()` + is new and runs deep-clone on both TellTarget references +- `ChatLogWindow` active-tab underline pill now scales with `ImGuiHelpers.GlobalScale` and rounds its DrawList + coordinates to physical pixels via `MathF.Round`, so the 2 px line stays crisp on 125 % and 150 % DPI setups instead + of bleeding into a sub-pixel blur +- `ImGuiUtil.IconButton` width parameter no longer subtracts HUD-scaled `CellPadding.X * 2` from the raw `int` width. + `ImGui.Button` handles its own frame padding internally, so the measured `buttonWidth` now passes through verbatim + (inspired-by upstream `f35b7d3`, but our two call-sites need the parameter, so the param itself stays) +- Internal: `HellionStyle` ChildBgAlpha threshold logic extracted to `HellionStyleHelpers.ResolveChildBgAlpha` with a + build-suite mirror test that pins the 0.999f cutoff. `Plugin.SaveConfig` clones only the temp-tab subset in the + pre-serialization snapshot instead of the full tab list. `SettingsOverview` caches `ImGui.GetWindowDrawList()` once + per frame and passes the pointer down to `DrawCard` +- Internal: `Dalamud.Utility.Util` static surface (`IsWine`, `OpenLink`) routed through a new `IPlatformUtil` + indirection. `MessageStore`'s `IsWine` probe is now reachable from the xUnit AppDomain via a `FakePlatformUtil` + fixture (full isolated MessageStore construction still pending — `Plugin.Log.Information` in `Migrate0` is a separate + Dalamud-static surface, slated for v1.4.7) + +Modding & support: join Hellion Forge — + +Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). + +--- + ## Hellion Chat 1.4.5 — UX and Robustness (2026-05-12) Sixth sub-patch of the v1.4.x polish-sweep series. User-visible robustness fixes plus two doc/test polish items from the diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f87088d..11271a7 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,14 +10,33 @@ the plugin's privacy-first scope during brainstorming. --- -## Next Cycle (v1.4.6) +## Next Cycle (v1.4.7) -**Code-Hygiene + Refactor.** Build-side pre-commit hook with csharpier-check as a hard gate so format drift can't reach -a commit (~30 min). Plus the cycle absorbs whatever surfaces from v1.4.5 smoke that doesn't justify a hotfix. Concrete -scope is consolidated in the v1.4.6 brainstorm. +**Backlog Cleanup.** Roll up the remaining audit items deferred from v1.4.0–v1.4.6 and the new entries surfaced during +v1.4.6 (notably the `Plugin.Log` indirection that would unlock fully isolated `MessageStore` construction tests, plus +follow-up scope hinted at in the ChatTwo upstream f35b7d3 cherry-picks). Scope is consolidated during brainstorm. --- +## v1.4.6 — Code Hygiene and Refactor (released 2026-05-12) + +Seventh sub-patch of the v1.4.x Polish Sweep series. Maintenance patch — no user-visible behaviour changes; tightens +the development feedback loop and pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier +reflow check (Block E) and a markdownlint pass (Block F), so style drift and markdown violations are blocked at the +pre-push gate. `FontManager.AddFontWithFallback` catch-filter now spans `InvalidOperationException` and +`ArgumentException` on top of the existing IO triad, with the exception type name in the warning log so the +diagnostic path can see which atlas-toolkit throw triggered the fallback. `BrandingLinks` and `IntegrationLinks` run a +`[ModuleInitializer]` URL validation pass on plugin load; a typo in a future URL rotation now throws at startup +instead of failing silently when a user clicks the broken button. Cherry-picked from ChatTwo upstream `f35b7d3`: +`Chat.SetChannel` no longer leaks the native `Utf8String` when the linkshell check rejects the channel (rename to +`IsChannelOrExistingLinkshell` plus wrap-not-return), and `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget` +(the previous reference copy let PopOut and Temp tabs mutate each other's channel state). The `ChatLogWindow` +active-tab underline pill scales with `ImGuiHelpers.GlobalScale` and rounds to physical pixels for crisp rendering +above 100 % DPI. Internal items: `HellionStyle` ChildBgAlpha extracted to a testable helper, `Plugin.SaveConfig` +clones only the temp-tab subset in the snapshot path, `SettingsOverview` caches the draw-list per frame, +`Dalamud.Utility.Util` static surface routed through an `IPlatformUtil` indirection (`MessageStore`'s `IsWine` probe +is now testable in isolation). No schema bump, no migration. + ## v1.4.5 — UX and Robustness (released 2026-05-12) Sixth sub-patch of the v1.4.x Polish Sweep series. User-visible robustness polish plus two doc/test polish items from From e33cf0dcb94dca57594ae4d385ed3109cbf692d1 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:58:59 +0200 Subject: [PATCH 16/18] ci(forge): add v1.4.6 forge announcement post DE body for the Hellion Forge Discord embed; subtitle and versionsnatur frontmatter fields within the 60/40 char caps; embed-total ~2267/5500 per the changelog-sync verifier. --- .github/forge-posts/v1.4.6.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/forge-posts/v1.4.6.md diff --git a/.github/forge-posts/v1.4.6.md b/.github/forge-posts/v1.4.6.md new file mode 100644 index 0000000..040e7b4 --- /dev/null +++ b/.github/forge-posts/v1.4.6.md @@ -0,0 +1,17 @@ +--- +subtitle: Code Hygiene and Refactor +versionsnatur: Maintenance-Cycle +--- + +Wartungs-Patch ohne User-sichtbare Änderungen. Saubere Code-Basis als Vorbereitung auf das v1.4.7-Backlog-Cleanup, plus zwei geerbte Bugfixes aus dem ChatTwo-Upstream `f35b7d3`. + +- **preflight.sh härter**: csharpier-Reflow-Check (Block E) und markdownlint (Block F) laufen jetzt im Pre-Push-Gate, statt erst beim Pre-Merge-Review aufzufallen. +- **FontManager-Fallback robuster**: Atlas-Toolkit-Throws aus kaputten Font-Configs (IO, InvalidOperation, ArgumentException) fallen jetzt zuverlässig auf NotoSansCjkRegular, statt den Atlas-Build mitzureißen. Der Exception-Typ wird im Log mitgegeben für die Diagnose. +- **URL-Validation beim Plugin-Load**: BrandingLinks (5 URLs) und IntegrationLinks (2 URLs) werden via `[ModuleInitializer]` geprüft. Ein Tippfehler bei einer künftigen URL-Rotation wirft jetzt sofort beim Plugin-Load, statt still beim Klick zu scheitern. +- **Cherry-Pick aus ChatTwo `f35b7d3`** — Memory-Leak in `Chat.SetChannel`: der native `Utf8String` wird jetzt auch dann freigegeben, wenn der Linkshell-Check den Channel ablehnt (vorher gefangen im early-return). +- **Cherry-Pick aus ChatTwo `f35b7d3`** — `Tab.Clone()` Deep-cloned jetzt `UsedChannel` und `TellTarget`. Vorher Reference-Share-Bug: PopOut- und Temp-Tabs mutierten sich gegenseitig. +- **Aktive-Tab-Underline pixel-perfect bei DPI-Scaling**: Die Underline-Pill skaliert jetzt mit `ImGuiHelpers.GlobalScale` und rundet die DrawList-Koordinaten auf physische Pixel. Kein Sub-Pixel-Blur mehr auf 125/150%-Setups. +- **IconButton-Width-Fix**: der manuelle `width - 2 * CellPadding.X`-Subtract verlor den HUD-Scale (Padding skaliert, der raw int nicht). Gemessene Breite läuft jetzt unverändert durch. +- **Test-Isolation für MessageStore**: `Dalamud.Utility.Util`-Surface (IsWine, OpenLink) läuft jetzt durch eine `IPlatformUtil`-Indirektion. MessageStores `IsWine`-Probe ist isoliert testbar in der Build-Suite. Plus: HellionStyle-ChildBgAlpha als Pure-Helper extrahiert, Plugin.SaveConfig kopiert nur Session-Tabs statt der ganzen Tab-Liste, SettingsOverview cached den DrawList einmal pro Frame. + +Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). From b46d3ad0a8ae4cbe49c3ff88f7e1104b270135ae Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 20:59:56 +0200 Subject: [PATCH 17/18] chore: bump schema-gate message to v1.4.6 Plugin.cs:171-172 hardcoded the version into the schema-gate InvalidOperationException string. The follow-up rename in v1.4.7 will move this to Plugin.Interface.Manifest.AssemblyVersion so this commit stops happening every cycle, but for v1.4.6 the bare version bump is the smallest change. Also picks up a one-line csharpier reflow on UrlValidation.cs collapsed by the format pass. --- HellionChat/Plugin.cs | 4 ++-- HellionChat/Util/UrlValidation.cs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 717e3fc..2863e82 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -168,8 +168,8 @@ public sealed class Plugin : IAsyncDalamudPlugin if (Config.Version < 16) { throw new InvalidOperationException( - $"HellionChat v1.4.5 requires config schema v16, got v{Config.Version}. " - + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.5." + $"HellionChat v1.4.6 requires config schema v16, got v{Config.Version}. " + + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.6." ); } diff --git a/HellionChat/Util/UrlValidation.cs b/HellionChat/Util/UrlValidation.cs index 98d49b7..279ce45 100644 --- a/HellionChat/Util/UrlValidation.cs +++ b/HellionChat/Util/UrlValidation.cs @@ -14,9 +14,7 @@ internal static class UrlValidation || (uri.Scheme is not "https" and not "http") ) { - throw new InvalidOperationException( - $"{source} contains malformed URL: {url}" - ); + throw new InvalidOperationException($"{source} contains malformed URL: {url}"); } } } From c5fe69f0d341c7ee88cd766a6adfa020ff0815ba Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 21:28:16 +0200 Subject: [PATCH 18/18] feat(themes): swap Moonlit Bloom for Crystal Nocturne, sort built-ins by colour family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crystal Nocturne (royal sapphire + electric magenta on obsidian, by CRYSTALLITE) replaces Moonlit Bloom in the built-in roster. The same chat-channel tinting convention applies: sapphire-blue identity on party/team channels, accent-magenta on tells, and an alternating mint/yellow/peach palette across the eight linkshell slots so each LS stays individually distinguishable on the dark obsidian background. Users who had Moonlit Bloom selected fall back to the default Hellion Arctic on the first plugin load. A custom JSON copy of Moonlit Bloom dropped into pluginConfigs/HellionChat/themes/ keeps working as a user theme. Plus a cosmetic re-sort of the registry: insertion order now drives a deliberate Theme-Picker grid layout (3 columns) — blue family in row 1, purple to magenta in row 2, green/warm/classic in row 3, Synthwave Sunset alone in row 4 as a retro bonus. --- .github/forge-posts/v1.4.6.md | 1 + HellionChat/HellionChat.yaml | 4 + HellionChat/Themes/Builtin/CrystalNocturne.cs | 82 +++++++++++++++++++ HellionChat/Themes/Builtin/MoonlitBloom.cs | 80 ------------------ HellionChat/Themes/ThemeRegistry.cs | 12 ++- README.md | 2 +- docs/CHANGELOG.md | 4 + docs/THEME-AUTHORING.md | 2 +- repo.json | 2 +- 9 files changed, 102 insertions(+), 87 deletions(-) create mode 100644 HellionChat/Themes/Builtin/CrystalNocturne.cs delete mode 100644 HellionChat/Themes/Builtin/MoonlitBloom.cs diff --git a/.github/forge-posts/v1.4.6.md b/.github/forge-posts/v1.4.6.md index 040e7b4..0285ae2 100644 --- a/.github/forge-posts/v1.4.6.md +++ b/.github/forge-posts/v1.4.6.md @@ -13,5 +13,6 @@ Wartungs-Patch ohne User-sichtbare Änderungen. Saubere Code-Basis als Vorbereit - **Aktive-Tab-Underline pixel-perfect bei DPI-Scaling**: Die Underline-Pill skaliert jetzt mit `ImGuiHelpers.GlobalScale` und rundet die DrawList-Koordinaten auf physische Pixel. Kein Sub-Pixel-Blur mehr auf 125/150%-Setups. - **IconButton-Width-Fix**: der manuelle `width - 2 * CellPadding.X`-Subtract verlor den HUD-Scale (Padding skaliert, der raw int nicht). Gemessene Breite läuft jetzt unverändert durch. - **Test-Isolation für MessageStore**: `Dalamud.Utility.Util`-Surface (IsWine, OpenLink) läuft jetzt durch eine `IPlatformUtil`-Indirektion. MessageStores `IsWine`-Probe ist isoliert testbar in der Build-Suite. Plus: HellionStyle-ChildBgAlpha als Pure-Helper extrahiert, Plugin.SaveConfig kopiert nur Session-Tabs statt der ganzen Tab-Liste, SettingsOverview cached den DrawList einmal pro Frame. +- **Built-in-Theme-Roster**: Crystal Nocturne (Royal Sapphire + Electric Magenta auf Obsidian, von CRYSTALLITE) ersetzt Moonlit Bloom. User mit Moonlit Bloom als aktivem Theme fallen beim ersten Plugin-Load auf Hellion Arctic zurück. Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index db7db94..549b301 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -65,6 +65,10 @@ changelog: |- Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation) + - Built-in themes: Crystal Nocturne (sapphire and electric + magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom. + Users with Moonlit Bloom selected fall back to Hellion Arctic + on first load Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). diff --git a/HellionChat/Themes/Builtin/CrystalNocturne.cs b/HellionChat/Themes/Builtin/CrystalNocturne.cs new file mode 100644 index 0000000..ad7cf5f --- /dev/null +++ b/HellionChat/Themes/Builtin/CrystalNocturne.cs @@ -0,0 +1,82 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class CrystalNocturne +{ + public const string Slug = "crystal-nocturne"; + + public static Theme Build() => + new( + Slug: Slug, + Name: "Crystal Nocturne", + Author: "CRYSTALLITE", + Description: "Royal sapphire and electric magenta over obsidian — a nocturne for the crystal-lit dance floor.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#1D4ED8"), + Primary: ColourUtil.HexToRgba("#3B82F6"), + PrimaryLight: ColourUtil.HexToRgba("#93C5FD"), + PrimaryGlow: ColourUtil.HexToRgba("#3B82F699"), + AccentDark: ColourUtil.HexToRgba("#A21CAF"), + Accent: ColourUtil.HexToRgba("#D946EF"), + AccentLight: ColourUtil.HexToRgba("#F0ABFC"), + Identity: ColourUtil.HexToRgba("#3B82F6"), + WindowBg: ColourUtil.HexToRgba("#08070F"), + ChildBg: ColourUtil.HexToRgba("#11101F"), + FrameBg: ColourUtil.HexToRgba("#1C1A33"), + Surface: ColourUtil.HexToRgba("#262340"), + SurfaceHover: ColourUtil.HexToRgba("#332D55"), + Border: ColourUtil.HexToRgba("#D946EF55"), + TextPrimary: ColourUtil.HexToRgba("#F5F3FF"), + TextMuted: ColourUtil.HexToRgba("#A5A0C0"), + TextDim: ColourUtil.HexToRgba("#4B4763"), + StatusSuccess: ColourUtil.HexToRgba("#10B981"), + StatusDanger: ColourUtil.HexToRgba("#F43F5E"), + StatusWarning: ColourUtil.HexToRgba("#FACC15"), + StatusInfo: ColourUtil.HexToRgba("#3B82F6") + ), + Layout: new ThemeLayout( + WindowRounding: 2f, + ChildRounding: 1f, + PopupRounding: 2f, + FrameRounding: 1f, + GrabRounding: 1f, + TabRounding: 1f, + ScrollbarRounding: 2f, + WindowBorderSize: 1f, + FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true, + ChatColors: new ThemeChatColors( + new Dictionary + { + // Crystal Nocturne — sapphire-blue identity for party/team channels, + // accent-magenta for tells, with mint/peach accents on linkshells + // so the eight LS slots stay individually distinguishable on the + // dark obsidian background. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F5F3FF"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FACC15"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0B090"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F0ABFC"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F0ABFC"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#93C5FD"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#10B981"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#93C5FD"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#10B981"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FACC15"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0ABFC"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#93C5FD"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#F0B090"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A5A0C0"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#D946EF"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#3B82F6"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#F0B090"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#F0B090"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A5A0C0"), + } + ) + ); +} diff --git a/HellionChat/Themes/Builtin/MoonlitBloom.cs b/HellionChat/Themes/Builtin/MoonlitBloom.cs deleted file mode 100644 index 544bb02..0000000 --- a/HellionChat/Themes/Builtin/MoonlitBloom.cs +++ /dev/null @@ -1,80 +0,0 @@ -using HellionChat.Util; - -namespace HellionChat.Themes.Builtin; - -internal static class MoonlitBloom -{ - public const string Slug = "moonlit-bloom"; - - public static Theme Build() => - new( - Slug: Slug, - Name: "Moonlit Bloom", - Author: "Hellion Forge", - Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.", - Colors: new ThemeColors( - PrimaryDark: ColourUtil.HexToRgba("#C957D0"), - Primary: ColourUtil.HexToRgba("#E374E8"), - PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"), - PrimaryGlow: ColourUtil.HexToRgba("#E374E899"), - AccentDark: ColourUtil.HexToRgba("#7AAC5C"), - Accent: ColourUtil.HexToRgba("#9CCB7C"), - AccentLight: ColourUtil.HexToRgba("#B6E297"), - Identity: ColourUtil.HexToRgba("#E374E8"), - WindowBg: ColourUtil.HexToRgba("#0E0C1F"), - ChildBg: ColourUtil.HexToRgba("#15122B"), - FrameBg: ColourUtil.HexToRgba("#1F1A38"), - Surface: ColourUtil.HexToRgba("#28224A"), - SurfaceHover: ColourUtil.HexToRgba("#332B5B"), - Border: ColourUtil.HexToRgba("#E374E844"), - TextPrimary: ColourUtil.HexToRgba("#ECE6F5"), - TextMuted: ColourUtil.HexToRgba("#9A8BB0"), - TextDim: ColourUtil.HexToRgba("#554B6E"), - StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"), - StatusDanger: ColourUtil.HexToRgba("#E85C6A"), - StatusWarning: ColourUtil.HexToRgba("#E8B590"), - StatusInfo: ColourUtil.HexToRgba("#6278FF") - ), - Layout: new ThemeLayout( - WindowRounding: 6f, - ChildRounding: 5f, - PopupRounding: 5f, - FrameRounding: 4f, - GrabRounding: 4f, - TabRounding: 4f, - ScrollbarRounding: 4f, - WindowBorderSize: 1f, - FrameBorderSize: 1f - ), - Typography: new ThemeTypography(), - IsBuiltIn: true, - ChatColors: new ThemeChatColors( - new Dictionary - { - // Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork - // und Linkshell4. Tell-Pink-Identität bleibt sichtbar. - [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"), - [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"), - [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"), - [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"), - [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"), - [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"), - [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"), - [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"), - [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"), - [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"), - [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"), - [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"), - [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"), - [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"), - [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"), - [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"), - [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"), - [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"), - [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"), - [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"), - [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"), - } - ) - ); -} diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs index ac26dd7..53b4610 100644 --- a/HellionChat/Themes/ThemeRegistry.cs +++ b/HellionChat/Themes/ThemeRegistry.cs @@ -15,17 +15,21 @@ public sealed class ThemeRegistry public ThemeRegistry(string? customThemesDir = null) { + // Insertion order drives the Theme-Picker grid layout (3 columns). + // Row 1: blue family. Row 2: purple to magenta family. + // Row 3: green / warm / classic. Row 4: Synthwave Sunset as a + // retro bonus on its own line. _builtIns = new Dictionary(StringComparer.OrdinalIgnoreCase) { { HellionArctic.Slug, HellionArctic.Build() }, { HellionSpectrum.Slug, HellionSpectrum.Build() }, - { Chat2Classic.Slug, Chat2Classic.Build() }, - { EventHorizon.Slug, EventHorizon.Build() }, - { MoonlitBloom.Slug, MoonlitBloom.Build() }, { NightBlue.Slug, NightBlue.Build() }, + { EventHorizon.Slug, EventHorizon.Build() }, { IndigoViolet.Slug, IndigoViolet.Build() }, - { ForgeMerchantman.Slug, ForgeMerchantman.Build() }, + { CrystalNocturne.Slug, CrystalNocturne.Build() }, { MintGrove.Slug, MintGrove.Build() }, + { ForgeMerchantman.Slug, ForgeMerchantman.Build() }, + { Chat2Classic.Slug, Chat2Classic.Build() }, { SynthwaveSunset.Slug, SynthwaveSunset.Build() }, }; diff --git a/README.md b/README.md index 7ddf9cc..46ddd17 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Hellion Chat is developed under **Hellion Forge**, the specialized modding and p #### Custom Themes (v1.1.0) HellionChat ships a theme engine with ten built-in themes (Hellion Arctic, Hellion Spectrum, Chat 2 Classic, Event -Horizon, Moonlit Bloom, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman, Synthwave Sunset) and a JSON-based +Horizon, Crystal Nocturne, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman, Synthwave Sunset) and a JSON-based authoring format for custom themes. Schema and step-by-step guide in [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md). Hellion Spectrum is Deuteranopia/Protanopia-safe (red-green color blindness) based on the Wong/Okabe-Ito palette. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b4b70ea..2374956 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -47,6 +47,10 @@ upstream-inherited bugs from ChatTwo `f35b7d3`, and prepares the code for the v1 indirection. `MessageStore`'s `IsWine` probe is now reachable from the xUnit AppDomain via a `FakePlatformUtil` fixture (full isolated MessageStore construction still pending — `Plugin.Log.Information` in `Migrate0` is a separate Dalamud-static surface, slated for v1.4.7) +- Built-in themes: Crystal Nocturne (royal sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces + Moonlit Bloom in the built-in roster. Users who had Moonlit Bloom selected fall back to the default Hellion Arctic + on the first plugin load; an existing custom JSON copy of Moonlit Bloom under `pluginConfigs/HellionChat/themes/` + keeps working unchanged Modding & support: join Hellion Forge — diff --git a/docs/THEME-AUTHORING.md b/docs/THEME-AUTHORING.md index 9fcee0c..5520b67 100644 --- a/docs/THEME-AUTHORING.md +++ b/docs/THEME-AUTHORING.md @@ -141,7 +141,7 @@ A theme can tint these toward its brand family (e.g., a purple theme can shift T **don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual hierarchy. -The eight colored built-in themes (Hellion Arctic, Hellion Spectrum, Event Horizon, Moonlit Bloom, Mint Grove, Night +The eight colored built-in themes (Hellion Arctic, Hellion Spectrum, Event Horizon, Crystal Nocturne, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman) all follow this rule — read their source for reference. Chat 2 Klassik intentionally ships without `chatChannels` so the user keeps their existing picks. diff --git a/repo.json b/repo.json index 6910163..81b8df0 100644 --- a/repo.json +++ b/repo.json @@ -14,7 +14,7 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.", - "Changelog": "**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two upstream-inherited bugs, and prepares the code for the v1.4.7 backlog cleanup.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- FontManager fallback catches the full set of atlas-toolkit throws (IO, InvalidOperation, ArgumentException) — a corrupt font config no longer takes down the whole atlas build\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now deep-clones UsedChannel and TellTarget — PopOut and Temp tabs no longer mutate each other's channel state\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- Internal: HellionStyle ChildBgAlpha extracted to a testable helper; Plugin.SaveConfig clones only the temp tabs; SettingsOverview caches the draw-list per frame; Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation)\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw failures surface as a notification, the first-run wizard has an explicit Later option, the input history clears on plugin reload, and the status bar version slot stops clipping in narrow windows.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- First-run wizard: explicit \"Later — keep defaults\" button. Closing the X no longer silently accepts the defaults; the wizard reopens on the next plugin load if nothing was picked\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system font if the embedded Hellion font resource is missing — logs a Warning\n\n---\n\n**v1.4.4 — Threading and IPC safety polish (2026-05-12)**\n\nFifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock falls away, and the privacy filter speaks up when an unknown ChatType shows up.\n\n- AutoTellTabs hot-path getter uses an Interlocked counter instead of taking the lock on every read\n- Honorific integration: per-method threading banners, plus Warning-level log on unsubscribe failure\n- AutoTranslate warmup thread marked IsBackground so plugin unload doesn't wait for it\n- PrivacyFilter logs once per unknown ChatType so a future patch's added channel doesn't drop off the radar\n- New installs persist unknown channels by default; existing configs keep their explicit choice\n\n---\n\n**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy startup work (migrations, hooks, windows) now runs async so Dalamud's UI stays responsive during load. Load time is comparable to v1.4.2 — this is the foundation for v1.4.4 optimisations.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases", + "Changelog": "**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two upstream-inherited bugs, and prepares the code for the v1.4.7 backlog cleanup.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- FontManager fallback catches the full set of atlas-toolkit throws (IO, InvalidOperation, ArgumentException) — a corrupt font config no longer takes down the whole atlas build\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now deep-clones UsedChannel and TellTarget — PopOut and Temp tabs no longer mutate each other's channel state\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- Internal: HellionStyle ChildBgAlpha extracted to a testable helper; Plugin.SaveConfig clones only the temp tabs; SettingsOverview caches the draw-list per frame; Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation)\n- Built-in themes: Crystal Nocturne (sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom. Users with Moonlit Bloom selected fall back to Hellion Arctic on first load\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw failures surface as a notification, the first-run wizard has an explicit Later option, the input history clears on plugin reload, and the status bar version slot stops clipping in narrow windows.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- First-run wizard: explicit \"Later — keep defaults\" button. Closing the X no longer silently accepts the defaults; the wizard reopens on the next plugin load if nothing was picked\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system font if the embedded Hellion font resource is missing — logs a Warning\n\n---\n\n**v1.4.4 — Threading and IPC safety polish (2026-05-12)**\n\nFifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock falls away, and the privacy filter speaks up when an unknown ChatType shows up.\n\n- AutoTellTabs hot-path getter uses an Interlocked counter instead of taking the lock on every read\n- Honorific integration: per-method threading banners, plus Warning-level log on unsubscribe failure\n- AutoTranslate warmup thread marked IsBackground so plugin unload doesn't wait for it\n- PrivacyFilter logs once per unknown ChatType so a future patch's added channel doesn't drop off the radar\n- New installs persist unknown channels by default; existing configs keep their explicit choice\n\n---\n\n**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy startup work (migrations, hooks, windows) now runs async so Dalamud's UI stays responsive during load. Load time is comparable to v1.4.2 — this is the foundation for v1.4.4 optimisations.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip", "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip",