From 11ad5db127277b7ddefcc2c4c20a4b58cc9de3f1 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 09:06:20 +0200 Subject: [PATCH 1/9] perf(autotell): replace lock-protected count with Interlocked counter F2.1: ActiveTempTabCount was doing a LINQ Count under _tempTabsLock on every read, including the hot-path HandleTell guard. Replace with an Interlocked counter kept in sync with Config.Tabs from inside the existing mutation paths (SpawnTempTab, DropOldestTempTab, OnLogout). Initialize from the persisted Tabs list on Initialize() to handle configs that already contain TempTabs from a prior session. Plugin.cs SaveConfig snapshot-restore mutates Config.Tabs outside of AutoTellTabsService; expose ResyncTempTabCounter() and call it after AddRange so the counter stays consistent. Plugin.cs:168 crash-recovery RemoveAll runs before Initialize() and is covered by the init snapshot. --- HellionChat/AutoTellTabsService.cs | 45 ++++++++++++++++++++++-------- HellionChat/Plugin.cs | 5 ++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index 8bc3436..64eb8f5 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using HellionChat.Code; @@ -19,6 +20,14 @@ internal sealed class AutoTellTabsService : IDisposable private readonly MessageStore _store; private readonly object _tempTabsLock = new(); + // F2.1: lock-free counter mirrors Config.Tabs.Count(IsTempTab) so the + // hot-path getter doesn't contend with HandleTell on every render frame. + // Bumped from inside the existing mutation paths so it stays consistent + // with the underlying list — see SpawnTempTab, DropOldestTempTab, OnLogout + // and ResyncTempTabCounter (used by Plugin.cs snapshot-restore). + // TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs + private int _activeTempTabCount; + private bool _initialized; internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store) @@ -28,16 +37,7 @@ internal sealed class AutoTellTabsService : IDisposable _store = store; } - internal int ActiveTempTabCount - { - get - { - lock (_tempTabsLock) - { - return Plugin.Config.Tabs.Count(t => t.IsTempTab); - } - } - } + internal int ActiveTempTabCount => Volatile.Read(ref _activeTempTabCount); internal void Initialize() { @@ -46,11 +46,31 @@ internal sealed class AutoTellTabsService : IDisposable return; } + // Seed the counter from the persisted Tabs list so a config that already + // contains TempTabs from a prior session starts in sync. Plugin.cs:168 + // crash-recovery has already dropped TempTabs by the time we get here, + // so the snapshot reflects post-recovery reality. + Interlocked.Exchange( + ref _activeTempTabCount, + Plugin.Config.Tabs.Count(t => t.IsTempTab) + ); + _messageManager.MessageProcessed += HandleTell; Plugin.ClientState.Logout += OnLogout; _initialized = true; } + // F2.1: callable from outside paths that mutate Config.Tabs directly + // (Plugin.cs snapshot-restore). Atomically re-pegs the counter to the + // live IsTempTab count. + internal void ResyncTempTabCounter() + { + Interlocked.Exchange( + ref _activeTempTabCount, + Plugin.Config.Tabs.Count(t => t.IsTempTab) + ); + } + public void Dispose() { if (!_initialized) @@ -184,6 +204,7 @@ internal sealed class AutoTellTabsService : IDisposable } Plugin.Config.Tabs.RemoveAt(victim.Index); + Interlocked.Decrement(ref _activeTempTabCount); // Re-anchor active tab to avoid silent switch when tab is dropped if (victim.Index <= _plugin.LastTab) @@ -208,6 +229,7 @@ internal sealed class AutoTellTabsService : IDisposable } Plugin.Config.Tabs.Add(tab); + Interlocked.Increment(ref _activeTempTabCount); } private static Tab BuildTempTab(string playerName, uint worldRowId) @@ -361,7 +383,8 @@ internal sealed class AutoTellTabsService : IDisposable } } - Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab); + var removed = Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab); + Interlocked.Add(ref _activeTempTabCount, -removed); // Force switch to tab 0 if active tab was temp or index is now out of range var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count; diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index a8411f5..0a33a1c 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -641,6 +641,11 @@ public sealed class Plugin : IAsyncDalamudPlugin Config.Tabs.Clear(); Config.Tabs.AddRange(snapshot); + + // 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. + AutoTellTabsService?.ResyncTempTabCounter(); } internal void LanguageChanged(string langCode) From 570a6f071c96313bb3a82a8b77670c0acbc12175 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 09:19:49 +0200 Subject: [PATCH 2/9] style(autotell): csharpier format F2.1 changes --- HellionChat/AutoTellTabsService.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index 64eb8f5..a3952b9 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -50,10 +50,7 @@ internal sealed class AutoTellTabsService : IDisposable // contains TempTabs from a prior session starts in sync. Plugin.cs:168 // crash-recovery has already dropped TempTabs by the time we get here, // so the snapshot reflects post-recovery reality. - Interlocked.Exchange( - ref _activeTempTabCount, - Plugin.Config.Tabs.Count(t => t.IsTempTab) - ); + Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab)); _messageManager.MessageProcessed += HandleTell; Plugin.ClientState.Logout += OnLogout; @@ -65,10 +62,7 @@ internal sealed class AutoTellTabsService : IDisposable // live IsTempTab count. internal void ResyncTempTabCounter() { - Interlocked.Exchange( - ref _activeTempTabCount, - Plugin.Config.Tabs.Count(t => t.IsTempTab) - ); + Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab)); } public void Dispose() From 5ca3b73b7ffcb06d06ea3c449781c6c73a04b453 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 09:19:52 +0200 Subject: [PATCH 3/9] refactor(honorific): per-method threading banners + warn on unsubscribe-fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F4.1: replace the block threading comment with per-method banners that read like documentation at the call site. F4.2: TryUnsubscribe now logs Warning instead of Debug — a silent unsubscribe failure leaks a live subscription across plugin reloads. F4.3: CurrentTitle gets a one-line banner matching the same convention. --- HellionChat/Integrations/HonorificService.cs | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/HellionChat/Integrations/HonorificService.cs b/HellionChat/Integrations/HonorificService.cs index a4467af..446d972 100644 --- a/HellionChat/Integrations/HonorificService.cs +++ b/HellionChat/Integrations/HonorificService.cs @@ -27,6 +27,7 @@ internal sealed class HonorificService : IDisposable private readonly IFramework _framework; private bool _versionWarningLogged; + // Thread: framework only — IPC delivery + ImGui render both run there. public HonorificTitleData? CurrentTitle { get; private set; } public bool IsAvailable { get; private set; } public (uint Major, uint Minor)? DetectedApiVersion { get; private set; } @@ -71,6 +72,7 @@ internal sealed class HonorificService : IDisposable TryUnsubscribe(() => _disposing.Unsubscribe(OnDisposing)); } + // Thread: framework (scheduled from ctor and OnReady). private void TryInitialPull() { try @@ -108,6 +110,7 @@ internal sealed class HonorificService : IDisposable } } + // Thread: framework (Dalamud IPC delivery contract). private void OnTitleChanged(string json) { // Skip updates on version mismatch; subscription stays live for reload. @@ -116,12 +119,13 @@ internal sealed class HonorificService : IDisposable CurrentTitle = ParseTitleJson(json); } + // Thread: any (Honorific dispatches NotifyReady from its own thread). private void OnReady() { - // Schedule on framework thread — NotifyReady can dispatch from any thread. _framework.RunOnFrameworkThread(TryInitialPull); } + // Thread: framework (IPC delivery contract); idempotent — Disposing fires once. private void OnDisposing() { // Honorific unloading — clear cached state so the header hides next frame. @@ -133,6 +137,8 @@ internal sealed class HonorificService : IDisposable DetectedApiVersion = null; } + // Thread: framework (called from Dispose, which runs on the framework + // cleanup block in Plugin.DisposeAsync). private void TryUnsubscribe(Action unsubscribe) { try @@ -141,18 +147,15 @@ internal sealed class HonorificService : IDisposable } catch (Exception ex) { - _log.Debug(ex, "Honorific unsubscribe failed (likely already gone)."); + // Warning not Debug — a silent unsubscribe failure leaks a live + // subscription across plugin reloads. + _log.Warning( + ex, + "Honorific unsubscribe failed (likely API break or gate already gone)." + ); } } - // Threading: IPC events and ImGui both run on the framework thread, so - // OnTitleChanged and the render path never race — no volatile/Interlocked - // needed as long as Dalamud's framework-thread delivery contract holds. - // - // Constructor and OnReady are exceptions: they run outside that contract - // (plugin-loader thread and Honorific's NotifyReady respectively), so both - // use RunOnFrameworkThread to safely reach ObjectTable.LocalPlayer. - // --- Pure-logic helpers; tested via HellionChat.Tests/Integrations. --- internal static HonorificTitleData? ParseTitleJson(string json) From 83064cd40b3bd50ed3bee890890c2f4b2701cf36 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 09:33:57 +0200 Subject: [PATCH 4/9] fix(autotranslate): mark warmup thread as IsBackground F9.2: PreloadCache spawned a new Thread without IsBackground, which kept the plugin unload blocked until the warmup finished (typically 100-300 ms). Setting IsBackground=true plus a named thread matches the pattern already used in MessageManager (F6.1) and Plugin.RetentionSweep (F9.3) since v1.4.0. --- HellionChat/Util/AutoTranslate.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/HellionChat/Util/AutoTranslate.cs b/HellionChat/Util/AutoTranslate.cs index e0a940c..fa4e0b5 100644 --- a/HellionChat/Util/AutoTranslate.cs +++ b/HellionChat/Util/AutoTranslate.cs @@ -54,15 +54,21 @@ internal static class AutoTranslate } // Warms the auto-translate cache on a background thread so the first - // message send doesn't hitch the main thread. + // message send doesn't hitch the main thread. IsBackground keeps plugin + // unload non-blocking even if the warmup is still in flight. internal static void PreloadCache() { - new Thread(() => + var thread = new Thread(() => { var sw = Stopwatch.StartNew(); AllEntries(); Plugin.Log.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms"); - }).Start(); + }) + { + IsBackground = true, + Name = "HellionChat-AutoTranslate-Warmup", + }; + thread.Start(); } private static List AllEntries() From 58e754c169096cc6b38d23b7c7937cab7d4214e0 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 09:41:52 +0200 Subject: [PATCH 5/9] feat(privacy): default PrivacyPersistUnknownChannels to true for new configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F3.1: future FFXIV patches can add new ChatTypes that aren't on any existing whitelist. With the field defaulted to false a new install would silently drop those channels until the user opts in. New configs now start with PrivacyPersistUnknownChannels=true via a constant in PrivacyDefaults. Existing configs keep their explicit choice — the deserializer overrides the initializer, so no migration and no schema bump. --- HellionChat/Configuration.cs | 8 ++++++-- HellionChat/Privacy/PrivacyDefaults.cs | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 5efebfd..d8bfd55 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -57,8 +57,12 @@ public class Configuration : IPluginConfiguration // Empty set means the migration has not run yet — see Plugin.cs v6→v7. public HashSet PrivacyPersistChannels = []; - // Failsafe for ChatTypes added by future FFXIV patches. - public bool PrivacyPersistUnknownChannels; + // Failsafe for ChatTypes added by future FFXIV patches. New configs default + // to the failsafe via PrivacyDefaults; existing configs keep their saved + // choice because the deserializer overrides this initializer. + public bool PrivacyPersistUnknownChannels = Privacy + .PrivacyDefaults + .DefaultPersistUnknownChannels; public bool IsAllowedForStorage(ChatType type) { diff --git a/HellionChat/Privacy/PrivacyDefaults.cs b/HellionChat/Privacy/PrivacyDefaults.cs index 2a3e503..ae5f569 100644 --- a/HellionChat/Privacy/PrivacyDefaults.cs +++ b/HellionChat/Privacy/PrivacyDefaults.cs @@ -4,6 +4,12 @@ namespace HellionChat.Privacy; internal static class PrivacyDefaults { + // F3.1: failsafe for ChatTypes added by future FFXIV patches. New installs + // persist unknown channels so a major patch's added ChatType isn't silently + // dropped before the user can opt in or out. Existing configs keep their + // explicit choice — see Configuration.cs PrivacyPersistUnknownChannels. + internal const bool DefaultPersistUnknownChannels = true; + // DSGVO Art. 25 (Privacy by Default): only the player's own conversations // are persisted out-of-the-box. Public chat, NPC dialogue, system logs and // battle messages require explicit opt-in. From 7eb50e2c8d616ed5c421e4c579dbaa24f846143d Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 09:54:05 +0200 Subject: [PATCH 6/9] feat(privacy): log warning on unknown ChatType in IsAllowedForStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F3.2: a future FFXIV patch can introduce ChatTypes that aren't on any existing whitelist, and the filter currently routes them silently through the unknown-channel failsafe. Add a dedup HashSet (per runtime, NonSerialized) so the first hit per ChatType logs a Warning. The failsafe behaviour itself is unchanged — only visibility is new. --- HellionChat/Configuration.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index d8bfd55..920caa3 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -64,12 +64,30 @@ public class Configuration : IPluginConfiguration .PrivacyDefaults .DefaultPersistUnknownChannels; + // F3.2: dedup unknown-ChatType warnings so a chatty filter doesn't spam + // the log every frame. NonSerialized so the warning fires once per + // runtime, not once-ever-per-install. + [NonSerialized] + private readonly HashSet _warnedUnknownChannels = new(); + public bool IsAllowedForStorage(ChatType type) { if (!PrivacyFilterEnabled) return true; if (PrivacyPersistChannels.Contains(type)) return true; + + // F3.2: log first occurrence so a new patch's ChatType doesn't drop + // off the radar. Failsafe still applies via PrivacyPersistUnknownChannels. + if (_warnedUnknownChannels.Add(type)) + { + Plugin.Log.Warning( + "PrivacyFilter: unknown ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.", + type, + PrivacyPersistUnknownChannels + ); + } + return PrivacyPersistUnknownChannels; } From 0fc88e480ab5368a702af087bfedd85684caf5a9 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 10:11:31 +0200 Subject: [PATCH 7/9] chore: bump version to 1.4.4 + changelog sync + forge-post Threading and IPC safety release. Items: F2.1 (Interlocked counter), F4.1/F4.2/F4.3 (HonorificService threading banners + warning log), F9.2 (AutoTranslate IsBackground), F3.1 (PrivacyPersistUnknownChannels default), F3.2 (unknown-ChatType warning). verify-changelog-sync: yaml/repo.json/forge-post in sync, embed-total ~2699/5500, 3/4 yaml subblocks. verify-version-consistency and verify-manifest-shape both green. --- .github/forge-posts/v1.4.4.md | 35 ++++++++++++++++++++++++++++++++++ HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 20 +++++++++++++++++++ docs/CHANGELOG.md | 32 +++++++++++++++++++++++++++++++ repo.json | 12 ++++++------ 5 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 .github/forge-posts/v1.4.4.md diff --git a/.github/forge-posts/v1.4.4.md b/.github/forge-posts/v1.4.4.md new file mode 100644 index 0000000..01bcfff --- /dev/null +++ b/.github/forge-posts/v1.4.4.md @@ -0,0 +1,35 @@ +--- +subtitle: Threading- und IPC-Sicherheits-Politur +versionsnatur: Wartung und Robustheit +--- + +**Hellion Chat 1.4.4 — Threading- und IPC-Sicherheits-Politur** + +Fünfter Sub-Patch der v1.4.x Polish-Sweep-Serie. Threading-Annahmen werden explizit pro Methode dokumentiert, ein +Hot-Path-Lock im Auto-Tell-Tab-Counter fällt weg, IPC-Cleanup wird sichtbar wenn er fehlschlägt und der Privacy-Filter +spricht jetzt bei unbekannten ChatTypes. + +- **AutoTellTabsService Hot-Path-Lock entfernt.** `ActiveTempTabCount` hat bisher pro Render-Frame ein LINQ-Count unter + einem Lock gemacht. Jetzt läuft das über einen Interlocked-Counter der parallel zur Tabs-Liste mitgeführt wird, + inklusive Resync-Hook für den Snapshot-Restore-Pfad in `SaveConfig`. Plus Pure-Helper-Test-Mirror in der Build-Suite + damit die Atomicity-Semantik nicht versehentlich wegrefactored wird +- **HonorificService selbst-dokumentierende Threading-Banner.** Statt eines Block-Comments am Klassen-Ende hat jede + IPC-Callback-Methode jetzt einen 1-Zeilen-Banner darüber, der den Thread-Kontext direkt am Call-Site benennt + (framework only, framework scheduled, any). Mehr Hilfe für künftige Reviews als ein abstraktes Threading-Kapitel +- **Unsubscribe-Failure ist jetzt sichtbar.** `TryUnsubscribe` hat ein Honorific-Unsubscribe-Failure bisher als Debug + geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription kann den Service über + Plugin-Reloads hinweg leben lassen, also läuft der Log jetzt auf Warning +- **AutoTranslate-Warmup blockiert den Plugin-Unload nicht mehr.** Der Cache-Warmup-Thread war ohne `IsBackground=true` + unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu MessageManager und RetentionSweep + (beide seit v1.4.0) +- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType einführt der weder + in der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der + Filter einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet, + also kein Log-Spam +- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen Configs jetzt auf `true`, + damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt wird bevor der User entscheiden kann. + Bestehende Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein + Schema-Bump + +Keine User-sichtbaren Funktions-Änderungen außer dem Default-Flip für neue Installationen. Settings, Themes, Tabs und +das Privacy-Verhalten für Bestand bleiben unangetastet. diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 67d73d1..570db82 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -1,7 +1,7 @@ - 1.4.3 + 1.4.4 enable enable diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index 13034d1..7fbb1f8 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -35,6 +35,26 @@ tags: - Replacement - Privacy changelog: |- + **v1.4.4 — Threading and IPC safety polish (2026-05-12)** + + Fifth 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. + + - AutoTellTabs hot-path getter uses an Interlocked counter + instead of taking the lock on every read + - Honorific integration: per-method threading banners, plus + Warning-level log on unsubscribe failure + - AutoTranslate warmup thread marked IsBackground so plugin + unload doesn't wait for it + - PrivacyFilter logs once per unknown ChatType so a future + patch's added channel doesn't drop off the radar + - New installs persist unknown channels by default; existing + configs keep their explicit choice + + --- + **v1.4.3 — Faster plugin load + new repo (2026-05-08)** Heavy startup work (migrations, hooks, windows) now runs async so diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8b4c171..7f880f4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,38 @@ to the release pages for details. --- +## Hellion Chat 1.4.4 — Threading and IPC Safety Polish (2026-05-12) + +Fifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock +falls away in `AutoTellTabsService`, IPC-cleanup failures become visible, and the privacy filter now speaks up when an +unknown ChatType shows up. + +- `AutoTellTabsService.ActiveTempTabCount` switches from a lock-protected LINQ `Count` to an `Interlocked` counter kept + in sync with `Config.Tabs` from inside the existing mutation paths. `Initialize()` seeds the counter from the + persisted Tabs list, and `SaveConfig`'s snapshot-restore path calls a new `ResyncTempTabCounter()` so the mid-step + `RemoveAll` doesn't leave the counter drifting. Pure-helper test mirror lives in the Build-Suite repo +- `HonorificService` per-method threading banners replace the block comment at the bottom of the file. Each IPC + callback (`TryInitialPull`, `OnTitleChanged`, `OnReady`, `OnDisposing`, `TryUnsubscribe`) and the `CurrentTitle` + field carry a one-line `// Thread:` annotation so the framework-thread invariant is visible at the call site +- `TryUnsubscribe` log-level upgraded from `Debug` to `Warning`. A silent unsubscribe failure leaks a live subscription + across plugin reloads, which is exactly the kind of issue that should not be at Debug +- `AutoTranslate.PreloadCache` thread now has `IsBackground = true` and a thread name. Without `IsBackground` the + warmup blocks plugin unload (typically 100-300 ms). Pattern-match to `MessageManager` (F6.1) and `Plugin.RetentionSweep` + (F9.3), both since v1.4.0 +- `Configuration.IsAllowedForStorage` adds a one-line `Plugin.Log.Warning` for the first occurrence of any ChatType + that isn't in `PrivacyPersistChannels`. Dedup via a `NonSerialized` `HashSet`, so the warning fires once + per runtime — not once per frame, not once per install. Failsafe routing through `PrivacyPersistUnknownChannels` + is unchanged +- `PrivacyPersistUnknownChannels` field default flipped from `false` to `true` for new installs via a constant in + `PrivacyDefaults`. Existing configs keep their explicit choice — the deserializer overrides the initializer. No + schema bump, no migration, no first-run banner + +Modding & support: join Hellion Forge — + +Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). + +--- + ## Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover (2026-05-08) Plugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin` API. The constructor now does only the bootstrap-essentials diff --git a/repo.json b/repo.json index d616320..bc26225 100644 --- a/repo.json +++ b/repo.json @@ -3,7 +3,7 @@ "Author": "Jon Kazama (Hellion Forge)", "Name": "Hellion Chat", "InternalName": "HellionChat", - "AssemblyVersion": "1.4.3.0", + "AssemblyVersion": "1.4.4.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.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.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", "AcceptsFeedback": true, - "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip", - "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip", - "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip", - "TestingAssemblyVersion": "1.4.3.0", + "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.4/latest.zip", + "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.4/latest.zip", + "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.4/latest.zip", + "TestingAssemblyVersion": "1.4.4.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 0982b68a4aba84d01e171b6f8e8065babbcf3bfe Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 10:22:21 +0200 Subject: [PATCH 8/9] chore: bump version references in Plugin.cs and README Pre-push grep-verification found four stale v1.4.3 mentions outside the Slim-Rule history files: - Plugin.cs schema-gate error message referenced v1.4.3 by name in both the comment and the user-facing exception text. Schema stays at v16, but the message now points at the current release - README.md latest-release badge bumped to v1.4.4 - README.md version header bumped to v1.4.4 - README.md Project Status block rewritten for v1.4.4 with the threading and IPC safety items as the lead ROADMAP.md historical references to v1.4.3 are intentional (released-tag, foundation-reference) and stay. --- HellionChat/Plugin.cs | 6 +++--- README.md | 22 ++++++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 0a33a1c..b3a8e5e 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -154,13 +154,13 @@ public sealed class Plugin : IAsyncDalamudPlugin Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); - // Schema gate: v1.4.3 requires config v16. Users on older schemas + // 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) { throw new InvalidOperationException( - $"HellionChat v1.4.3 requires config schema v16, got v{Config.Version}. " - + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.3." + $"HellionChat v1.4.4 requires config schema v16, got v{Config.Version}. " + + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.4." ); } diff --git a/README.md b/README.md index 10d416c..9ca9a87 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.3-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest) +[![Latest release](https://img.shields.io/badge/release-v1.4.4-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.3** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on +**Version 1.4.4** — 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,14 +286,16 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo ## Project Status -**Version 1.4.3** — Plugin-load async init plus repo cutover: the plugin has been migrated to Dalamud's -`IAsyncDalamudPlugin` API. The constructor now handles only bootstrap essentials (config load, language init, conflict -detection); migrations, service allocations, window construction, and hook subscriptions move into `LoadAsync`, allowing -Dalamud to keep the UI responsive during heavy lifting. A schema gate replaces the v9 → v16 migration chain; configs at -schema v16+ load directly, older configs trigger an "install v1.4.2 first" error message. Custom repo URL migrated to -`gitea.hellion-forge.cloud`; the GitHub repo remains as a frozen v1.4.2 snapshot. Plugin load time is ~3.7 s median (5 -reloads), comparable to v1.4.2 — the async migration is the foundation for v1.4.4 lazy-init optimizations, not a direct -user-facing win. Fourth sub-patch of the v1.4.x polish sweep series (as of 2026-05-08). +**Version 1.4.4** — Threading and IPC safety polish on top of the v1.4.3 async-load foundation. The +`AutoTellTabsService` hot-path getter now reads from an `Interlocked` counter instead of taking a lock on every +render frame, with a resync hook for the snapshot-restore path in `SaveConfig` and a pure-helper test mirror in the +Build-Suite repo. The Honorific integration carries per-method threading banners so the framework-thread invariant is +visible at the call site, and an unsubscribe failure now logs at Warning instead of Debug — a leaked subscription +across plugin reloads is exactly the kind of thing that should not be silent. The AutoTranslate warmup thread is +finally marked `IsBackground = true`, matching the pattern used in `MessageManager` and `Plugin.RetentionSweep` since +v1.4.0. The privacy filter logs once per unknown ChatType so a future patch's added channel does not drop off the +radar, and new installs default `PrivacyPersistUnknownChannels` to `true` as a failsafe; existing configs keep their +explicit choice. No schema bump, no migration. Fifth 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: From 57da4557002a7fd31048348352e932d5e31d7bbc Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 12 May 2026 10:47:43 +0200 Subject: [PATCH 9/9] fix: post-review polish on v1.4.4 - IsAllowedForStorage warning now only fires for ChatTypes the build doesn't recognise (Enum.IsDefined), not for opted-out known ones - Drop stale tests-location comment in HonorificService --- HellionChat/Configuration.cs | 10 ++++++---- HellionChat/Integrations/HonorificService.cs | 2 -- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 920caa3..c463467 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -77,12 +77,14 @@ public class Configuration : IPluginConfiguration if (PrivacyPersistChannels.Contains(type)) return true; - // F3.2: log first occurrence so a new patch's ChatType doesn't drop - // off the radar. Failsafe still applies via PrivacyPersistUnknownChannels. - if (_warnedUnknownChannels.Add(type)) + // F3.2: log first occurrence of a ChatType the running build doesn't + // recognise — i.e. one a future FFXIV patch may have added. Known + // types the user opted out of are routed through the failsafe + // silently, like before. + if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type)) { Plugin.Log.Warning( - "PrivacyFilter: unknown ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.", + "PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.", type, PrivacyPersistUnknownChannels ); diff --git a/HellionChat/Integrations/HonorificService.cs b/HellionChat/Integrations/HonorificService.cs index 446d972..73b47d3 100644 --- a/HellionChat/Integrations/HonorificService.cs +++ b/HellionChat/Integrations/HonorificService.cs @@ -156,8 +156,6 @@ internal sealed class HonorificService : IDisposable } } - // --- Pure-logic helpers; tested via HellionChat.Tests/Integrations. --- - internal static HonorificTitleData? ParseTitleJson(string json) { if (string.IsNullOrEmpty(json))