Merge feature/v1.4.4 into main (Threading and IPC Safety release)
Security / scan (push) Successful in 18s

This commit is contained in:
2026-05-12 10:56:51 +02:00
12 changed files with 196 additions and 48 deletions
+35
View File
@@ -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.
+28 -11
View File
@@ -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,25 @@ 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 +198,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 +223,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 +377,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;
+26 -2
View File
@@ -57,8 +57,18 @@ public class Configuration : IPluginConfiguration
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
public HashSet<ChatType> 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;
// 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<ChatType> _warnedUnknownChannels = new();
public bool IsAllowedForStorage(ChatType type)
{
@@ -66,6 +76,20 @@ public class Configuration : IPluginConfiguration
return true;
if (PrivacyPersistChannels.Contains(type))
return true;
// 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: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
type,
PrivacyPersistUnknownChannels
);
}
return PrivacyPersistUnknownChannels;
}
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
<PropertyGroup>
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
<Version>1.4.3</Version>
<Version>1.4.4</Version>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Use lock file to pin exact versions -->
+20
View File
@@ -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
+13 -12
View File
@@ -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,20 +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)
{
if (string.IsNullOrEmpty(json))
+8 -3
View File
@@ -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."
);
}
@@ -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)
+6
View File
@@ -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.
+9 -3
View File
@@ -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<AutoTranslateEntry> AllEntries()
+12 -10
View File
@@ -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 @@
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
</p>
**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:
+32
View File
@@ -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<ChatType>`, 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 — <https://discord.gg/X9V7Kcv5gR>
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
+6 -6
View File
@@ -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. 25% 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. 25% 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",