diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs
index 7ee6456..8bc3436 100644
--- a/HellionChat/AutoTellTabsService.cs
+++ b/HellionChat/AutoTellTabsService.cs
@@ -10,14 +10,8 @@ using HellionChat.Util;
namespace HellionChat;
-// Hellion Chat — Auto-Tell-Tabs.
-//
-// Spawns a session-only tab per /tell partner so a club greeter can track
-// multiple parallel conversations without losing context. Subscribes to
-// MessageManager.MessageProcessed for live tells and to ClientState.Logout
-// for the cleanup pass; everything else hangs off these two entry points.
-//
-// See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian vault).
+// Auto-Tell-Tabs: spawns session-only tabs per /tell partner.
+// Subscribes to MessageManager.MessageProcessed and ClientState.Logout.
internal sealed class AutoTellTabsService : IDisposable
{
private readonly Plugin _plugin;
@@ -87,10 +81,7 @@ internal sealed class AutoTellTabsService : IDisposable
var partner = ExtractTellPartner(message);
if (partner == null)
{
- // Real message without a player payload — e.g. GM tells, which
- // we deliberately skip. The diagnostics make future regressions
- // (FFXIV changing tell payload shape, new edge cases) findable
- // without having to crank up debug logging at the source.
+ // Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
Plugin.Log.Warning(
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
@@ -105,9 +96,7 @@ internal sealed class AutoTellTabsService : IDisposable
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
if (existing != null)
{
- // Tab already exists; Tab.Matches has already routed this
- // message via the MessageManager pipeline (see Task 2 sender
- // filter).
+ // Already routed via MessageManager pipeline
return;
}
@@ -124,10 +113,7 @@ internal sealed class AutoTellTabsService : IDisposable
{
if (message.Code.Type == ChatType.TellIncoming)
{
- // Incoming tell: the sender is the conversation partner. The
- // PlayerPayload normally rides on a chunk's Link slot, but for
- // some tell types FFXIV only puts it in the raw SeString —
- // fall back to that before giving up.
+ // Sender is the partner; check chunks first, then raw SeString as fallback
var fromSender =
ChunkUtil.TryGetPlayerPayload(message.Sender)
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
@@ -138,10 +124,7 @@ internal sealed class AutoTellTabsService : IDisposable
return null;
}
- // Outgoing tell: the local player is the sender, the partner shows
- // up either as a payload in the content (for tells typed via the
- // Chat 2 input bar) or as the channel's tracked tell target (set by
- // the SetContextTellTarget game hook). Same SeString fallback.
+ // Outgoing tell: check content first, then channels's TellTarget as fallback
var fromContent =
ChunkUtil.TryGetPlayerPayload(message.Content)
?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
@@ -175,10 +158,7 @@ internal sealed class AutoTellTabsService : IDisposable
private void DropOldestTempTab()
{
- // Greeted tabs are dropped before un-greeted ones (the user said
- // "I'm done with that conversation"), and within each bucket we
- // pick the oldest LastActivity. This protects active conversations
- // and unfinished greetings while still freeing up a slot.
+ // Prioritize greeted tabs for drop; within each bucket, drop by oldest LastActivity
var victim = Plugin
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
.Where(t => t.Tab.IsTempTab)
@@ -191,12 +171,7 @@ internal sealed class AutoTellTabsService : IDisposable
return;
}
- // v0.6.1 — if the victim is currently popped out, tear down the
- // matching Popout window first. Otherwise the window stays in
- // PopOutWindows + WindowSystem and renders empty / re-spawns on the
- // next AddPopOutsToDraw tick. Latent since pop-outs were introduced;
- // becomes visible with AutoTellTabsOpenAsPopout where dropping a
- // popped tab is now a routine code path.
+ // Clean up pop-out window if tab is popped out
if (victim.Tab.PopOut)
{
var popout = _plugin.ChatLogWindow.ActivePopouts.FirstOrDefault(p =>
@@ -210,8 +185,7 @@ internal sealed class AutoTellTabsService : IDisposable
Plugin.Config.Tabs.RemoveAt(victim.Index);
- // Re-anchor the active tab so the user does not silently end up on
- // a different conversation when their tab gets dropped or shifted.
+ // Re-anchor active tab to avoid silent switch when tab is dropped
if (victim.Index <= _plugin.LastTab)
{
_plugin.WantedTab = 0;
@@ -222,22 +196,12 @@ internal sealed class AutoTellTabsService : IDisposable
{
var tab = BuildTempTab(partner.Name, partner.World);
- // Preload first so the tab opens with chronological history above
- // the current message — and so a slow DB query never causes a
- // visible "empty tab, then history pops in" effect on screen.
- // The current message is already persisted in the store by the
- // time MessageProcessed fires (see MessageManager.cs: UpsertMessage
- // runs before the event), so we have to exclude it explicitly to
- // avoid the separator landing below the live tell.
+ // Preload history: chronological order with current message already persisted
PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);
tab.AddMessage(currentMessage, unread: true);
- // Hellion Chat v0.6.1 — opt-in: open new /tell tabs directly as a
- // pop-out window. Set BEFORE Tabs.Add so the next render-tick's
- // AddPopOutsToDraw() sees PopOut=true and spawns the Popout window
- // alongside the tab going into the list. No SaveConfig() because
- // auto-tell tabs are IsTempTab (session-only, never persisted).
+ // Open as pop-out if configured (set before Tabs.Add for next render-tick)
if (Plugin.Config.AutoTellTabsOpenAsPopout)
{
tab.PopOut = true;
@@ -272,9 +236,7 @@ internal sealed class AutoTellTabsService : IDisposable
{
return $"{playerName}@{worldRow.Name}";
}
- // World sheet lookup miss is rare (only for FFXIV worlds Dalamud has
- // not yet seen). Fall back to the raw RowId so the user still has a
- // unique, readable label.
+ // Fallback if world lookup misses (rare; only for unseen worlds)
return $"{playerName}@World{worldRowId}";
}
@@ -288,9 +250,7 @@ internal sealed class AutoTellTabsService : IDisposable
try
{
- // Pull one extra row because the live tell that triggered this
- // spawn is already in the store and would otherwise eat one of
- // the user's preload-budget slots.
+ // Pull one extra row: current message is already in store and would eat a preload slot
var history = _store.GetTellHistoryWithSender(
_messageManager.CurrentContentId,
senderName,
@@ -305,23 +265,17 @@ internal sealed class AutoTellTabsService : IDisposable
if (historicMessages.Count == 0)
{
- // No prior tells with this player — leave the tab to start
- // empty so the user does not see a "history loaded" marker
- // sitting alone above the very first message.
+ // No prior tells; leave tab empty to avoid orphaned "history loaded" marker
return;
}
- // The history list is already oldest-first, so a plain AddPrune
- // loop produces the chronological order the user expects to see
- // when the tab opens.
+ // History is oldest-first; add in order for chronological display
foreach (var message in historicMessages)
{
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
}
- // Visible separator between the loaded history and the live
- // tell that triggered this spawn. Goes in last so it sorts
- // after the historical messages but before the current one.
+ // Separator between history and live tell (sorts after history but before current)
tab.Messages.AddPrune(
MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator),
MessageManager.MessageDisplayLimit
@@ -329,9 +283,7 @@ internal sealed class AutoTellTabsService : IDisposable
}
catch (Exception ex)
{
- // Non-fatal: the tab still spawns, but the user gets a visible
- // notice instead of silently missing history. The error logs
- // once with full stack trace for diagnosis.
+ // Non-fatal: tab still spawns with visible error notice instead of silent history loss
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
tab.Messages.AddPrune(
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
@@ -372,9 +324,7 @@ internal sealed class AutoTellTabsService : IDisposable
lock (_tempTabsLock)
{
- // Frame-race guard (E5): the sidebar might still render a tab
- // that has already been removed by LRU drop or logout cleanup.
- // Silently skip the toggle so we don't mutate stale state.
+ // Guard against frame-race: sidebar might render a tab already removed by LRU or logout
if (!Plugin.Config.Tabs.Contains(tab))
{
return;
@@ -388,18 +338,12 @@ internal sealed class AutoTellTabsService : IDisposable
{
lock (_tempTabsLock)
{
- // Snapshot whether the active tab is about to be removed, BEFORE
- // we mutate the list — index lookups would lie to us afterwards.
+ // Snapshot active tab index before mutating list
var lastIndex = _plugin.LastTab;
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
- // v0.6.1 — symmetric to DropOldestTempTab cleanup: tear down any
- // popped-out temp tab windows before removing the tabs themselves,
- // otherwise PopOutWindows + WindowSystem keep ghost entries until
- // the next plugin reload. Especially relevant once Auto-Pop-Out is
- // enabled — every logout would otherwise leak as many ghosts as
- // there were active /tell pop-outs.
+ // Clean up pop-out windows before removing temp tabs
var poppedTempTabIds = Plugin
.Config.Tabs.Where(t => t.IsTempTab && t.PopOut)
.Select(t => t.Identifier)
@@ -419,9 +363,7 @@ internal sealed class AutoTellTabsService : IDisposable
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
- // Force a switch to tab 0 if the active tab was a temp tab OR
- // if drops before the active index pushed LastTab out of range.
- // Otherwise the user keeps their current persistent tab.
+ // 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;
if (currentWasTempTab || !stillValid)
{
diff --git a/HellionChat/Branding/BrandingLinks.cs b/HellionChat/Branding/BrandingLinks.cs
index 45adc57..c6eec3c 100644
--- a/HellionChat/Branding/BrandingLinks.cs
+++ b/HellionChat/Branding/BrandingLinks.cs
@@ -1,11 +1,12 @@
-// HellionChat/Branding/BrandingLinks.cs
namespace HellionChat.Branding;
-// Centralised so a future invite rotation only touches one file. The same
-// link is currently hard-coded in repo.json, README.md, SUPPORT.md,
-// CONTRIBUTORS.md and HellionChat.yaml — those will be migrated to consume
-// this constant in a separate housekeeping sweep
+// Centralised — a future invite/URL rotation only touches this file.
internal static class BrandingLinks
{
public const string HellionForgeDiscordInvite = "https://discord.gg/X9V7Kcv5gR";
+ public const string HellionForgeGitea = "https://gitea.hellion-forge.cloud/Hellion-Forge";
+ public const string HellionChatRepo =
+ "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";
}
diff --git a/HellionChat/Chunk.cs b/HellionChat/Chunk.cs
index 9f943d0..042df73 100755
--- a/HellionChat/Chunk.cs
+++ b/HellionChat/Chunk.cs
@@ -34,9 +34,7 @@ public abstract class Chunk
_ => null,
};
- ///
- /// Get some basic text for use in generating hashes.
- ///
+ // Returns basic text for hashing (content for TextChunk, icon name for IconChunk)
internal string StringValue()
{
return this switch
@@ -108,9 +106,6 @@ public class TextChunk : Chunk
Content = content ?? "";
}
- ///
- /// Creates a new TextChunk with identical styling to this one.
- ///
public TextChunk NewWithStyle(ChunkSource source, Payload? link, string content)
{
return new TextChunk(source, link, content)
@@ -122,9 +117,6 @@ public class TextChunk : Chunk
};
}
- ///
- /// Creates a new TextChunk with identical styling to this one.
- ///
public TextChunk NewWithStyle(Chunk chunk, string content)
{
return new TextChunk(chunk, content)
diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs
index 0465041..5efebfd 100755
--- a/HellionChat/Configuration.cs
+++ b/HellionChat/Configuration.cs
@@ -38,33 +38,26 @@ public class Configuration : IPluginConfiguration
public int Version { get; set; } = LatestVersion;
- // v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
+ // Slug-based; ThemeRegistry resolves the object at runtime.
public string Theme = "hellion-arctic";
- // v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
- // HellionThemeWindowOpacity beim Bump v13 → v14.
+ // Global window opacity, applied across all themes.
public float WindowOpacity = 0.85f;
- // v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden
- // vorab angelegt, damit später keine Migration nötig ist.
+ // Reserved for future UI toggles; pre-declared to avoid a migration later.
public bool ReduceMotion;
- // v1.2.1 — Default geflippt von false → true. Card-Rows-Layout aus
- // v1.2.0 wurde als zu dicht empfunden; Single-Line `[HH:mm] Sender:
- // Text` ist besser lesbar und platzsparender. Bestand-User mit aktiv
- // false werden durch die v15→v16-Migration auf den neuen Default
- // gehoben (Heuristik: wer in v1.2.0 false hatte, hatte den damals
- // neu eingeführten Default — kaum jemand hat aktiv abgeschaltet).
+ // v1.2.1: default flipped false → true. Compact single-line layout is
+ // more readable than the card-rows layout introduced in v1.2.0.
public bool UseCompactDensity = true;
- // Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
- // Master-switch defaults to true; set false to restore upstream behavior.
+ // Privacy by Default master switch. Set false to restore upstream behaviour.
public bool PrivacyFilterEnabled = true;
// 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 we don't know about.
+ // Failsafe for ChatTypes added by future FFXIV patches.
public bool PrivacyPersistUnknownChannels;
public bool IsAllowedForStorage(ChatType type)
@@ -76,79 +69,23 @@ public class Configuration : IPluginConfiguration
return PrivacyPersistUnknownChannels;
}
- // Hellion Chat — Message retention (GDPR data minimization, time axis).
- // Master switch defaults to false; the plugin will not delete history
- // until the user explicitly opts in.
+ // Retention master switch defaults to false — plugin will not delete
+ // history until the user explicitly opts in.
public bool RetentionEnabled;
public int RetentionDefaultDays = 30;
public Dictionary RetentionPerChannelDays = [];
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
-
- // Hellion Chat first-run wizard — opens once on a fresh install. Existing
- // ChatTwo users skip it because the v6→v7 migration sets the flag.
public bool FirstRunCompleted;
-
- // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
- // instead of whatever GlobalFontV2.FontId points at. Default ON so a
- // fresh install gets the Hellion typography out-of-the-box; flip OFF
- // to fall back to the user's chosen system or Dalamud font.
public bool UseHellionFont = true;
-
- // Cycle 1 of the plugin-integration roadmap. When Honorific is installed
- // and reports a custom title, render it in the chat header above the
- // message log. Auto-hides regardless when Honorific is missing or the
- // active title is original/empty, so leaving this on is safe even for
- // users who don't run Honorific.
public bool ShowHonorificTitleInHeader = true;
-
- // Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing
- // /tell spawns a session-only tab dedicated to that conversation
- // partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian).
public bool EnableAutoTellTabs = true;
-
- // Hard cap on simultaneously open auto tell tabs. Range enforced by the
- // settings slider (1–50). LRU drop favors greeted tabs first.
public int AutoTellTabsLimit = 15;
-
- // When true the sidebar shows only a thin separator before the temp
- // tabs; when false a section header "Active Tells (n)" is rendered.
public bool AutoTellTabsCompactDisplay;
-
- // Number of prior tells to preload from the message store when an
- // auto tell tab is spawned. Range 0–100; 0 disables preload.
public int AutoTellTabsHistoryPreload = 20;
-
- // Show the greeter "marked-as-greeted" toggle button next to each
- // temp tab and dim the tab name when set. Off by default because the
- // workflow is specific to club-greeter use cases — most users just
- // want the auto tabs themselves without the extra UI affordance.
public bool AutoTellTabsShowGreetedToggle;
-
- // Hellion Chat — One-Time-Hint-Banner that introduces the v0.6.0 pop-out
- // input feature. Set to true once the user dismisses the banner from a
- // pop-out window; never reset after that.
public bool SeenPopOutInputHint;
-
- // Hellion Chat — v0.6.0 master switch for the pop-out input bar.
- // Global on purpose: per-tab makes no sense for Auto-Tell-Tabs which
- // are session-only and would force the user to re-enable it for every
- // new conversation. Default flipped to ON in v0.6.1 (was OFF in v0.6.0)
- // because tester feedback called the manual toggle "umständlich, wirkt
- // unfertig". v11 → v12 migration applies the same flip to existing users.
public bool PopOutInputEnabled = true;
-
- // Hellion Chat — v0.6.1 One-Time-Hint-Banner that introduces the
- // chat-header pop-out toolbar button and reminds about the pop-out
- // input default flip. Set to true once the user dismisses the banner
- // from the main chat window; never reset after that.
public bool SeenPopOutHeaderHint;
-
- // Hellion Chat — v0.6.1 opt-in: when true, AutoTellTabsService.SpawnTempTab
- // sets tab.PopOut = true on every new auto-tell tab so the conversation
- // pops out as its own window directly. Closing the pop-out returns the
- // tab to the sidebar via the standard Popout.OnClose() flow. Default OFF
- // because the existing sidebar workflow is what most users (especially
- // club greeters tracking many parallel tells) expect by default.
public bool AutoTellTabsOpenAsPopout;
public int GetRetentionDays(ChatType type)
@@ -167,10 +104,7 @@ public class Configuration : IPluginConfiguration
public bool HideInLoadingScreens;
public bool HideInBattle;
- // v1.2.1 — Default geflippt false → true. Hellion-UI im NG+-Menü
- // versteckt zu halten ist konsistent mit den anderen Hide-Defaults
- // (Cutscenes, Logged-out, UI-Hidden) — UI-out-of-the-way bei Story-
- // Sequenzen.
+ // v1.2.1: default flipped false → true for consistency with other hide defaults.
public bool HideInNewGamePlusMenu = true;
public bool HideWhenInactive;
public int InactivityHideTimeout = 10;
@@ -186,18 +120,8 @@ public class Configuration : IPluginConfiguration
public bool NativeItemTooltips = true;
public bool PrettierTimestamps = true;
public bool MoreCompactPretty;
-
- // v1.2.1 — Default geflippt false → true. Wiederholte Zeitstempel
- // innerhalb derselben Minute lesen sich als Rauschen; ein einziger
- // Timestamp pro Minute reicht aus um die Konversation zu verorten.
public bool HideSameTimestamps = true;
public bool ShowNoviceNetwork;
-
- // Hellion Chat — vertical sidebar tab layout reads better than the
- // horizontal tab strip in the company of Auto-Tell-Tabs (a club
- // greeter typically tracks 5–15 simultaneous conversations). Bestand
- // users keep their saved value untouched — only fresh installs pick
- // up the new default.
public bool SidebarTabView = true;
public bool PrintChangelog = true;
public bool OnlyPreviewIf;
@@ -218,22 +142,10 @@ public class Configuration : IPluginConfiguration
public bool CollapseKeepUniqueLinks;
public bool PlaySounds = true;
public bool KeepInputFocus = true;
-
- // v1.2.1 — Default gesenkt 5000 → 2500. 5000 ist auf Mid-Range-
- // Hardware bei langen Sessions spürbar langsamer (Card-Layout
- // re-Layout pro Frame), 2500 deckt eine typische Stunde Chat ab
- // und bleibt smooth. User die mehr brauchen können bis 10000 hoch.
public int MaxLinesToRender = 2_500; // 1-10000
-
- // Default ON to match a German / European 24h locale. The
- // ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via
- // CultureInfo.InvariantCulture so the result is consistent across
- // host locales.
public bool Use24HourClock = true;
-
public bool ShowEmotes = true;
public HashSet BlockedEmotes = [];
-
public bool FontsEnabled = true;
public ExtraGlyphRanges ExtraGlyphRanges = 0;
public float FontSizeV2 = 12.75f;
@@ -258,12 +170,6 @@ public class Configuration : IPluginConfiguration
public float TooltipOffset;
- // v1.2.1 — Default-Chat-Farben sind das Hellion-Brand-Preset. Der
- // First-Run-Wizard bietet keine Theme-/Preset-Wahl an, daher kriegen
- // neue User die Hellion-Brand-Farben out-of-the-box (Cyan-Familie für
- // Standard/Tell, Ember/Warning für laute Channels). Bestand-User mit
- // leerem ChatColours-Dict werden durch die v15→v16-Migration auf das
- // Preset gehoben; User die bereits Custom-Farben haben, bleiben.
public Dictionary ChatColours = BuildDefaultChatColours();
private static Dictionary BuildDefaultChatColours()
@@ -333,9 +239,7 @@ public class Configuration : IPluginConfiguration
MaxLinesToRender = other.MaxLinesToRender;
Use24HourClock = other.Use24HourClock;
ShowEmotes = other.ShowEmotes;
- // Deep-copy the set so the live and mutable Configuration instances don't share state
- // — a HashSet reference assignment would cause edits in the settings window to leak
- // into the live config before the user clicks Save.
+ // Deep-copy so settings window edits don't leak into live config before Save.
BlockedEmotes = new HashSet(other.BlockedEmotes);
FontsEnabled = other.FontsEnabled;
ItalicEnabled = other.ItalicEnabled;
@@ -349,22 +253,11 @@ public class Configuration : IPluginConfiguration
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
- // Hellion Chat — Auto-Tell-Tabs are session-only and therefore
- // never present in a disk-loaded copy. Keep the live temp tabs of
- // *this* configuration alive across an UpdateFrom so a settings
- // save (or sidebar-mode toggle) does not silently destroy the
- // user's open tell conversations.
- //
- // For persistent tabs we go through Tab.Clone() which intentionally
- // does NOT copy the NonSerialized Messages list (avoids shared
- // mutable state on disk-load). On a settings save that means the
- // chat history for every persistent tab would be wiped — bug
- // reported by Flo 2026-05-05. We work around it by capturing the
- // live MessageList (and LastSendUnread counter) by Identifier
- // before the replace, then restoring it onto the freshly cloned
- // tabs whose Identifier survives Tab.Clone(). New tabs added in
- // settings get a fresh empty MessageList; deleted tabs lose their
- // history (intended).
+ // Keep live temp tabs alive across UpdateFrom — a settings save must
+ // not destroy open tell conversations. For persistent tabs, capture
+ // the live MessageList and LastSendUnread by Identifier before the
+ // replace and restore them onto the freshly cloned tabs; new tabs
+ // get an empty MessageList, deleted tabs lose their history (intended).
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
var livePersistentSession = Tabs.Where(t => !t.IsTempTab)
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
@@ -456,9 +349,7 @@ public class Tab
{
public string Name = Language.Tab_DefaultName;
- // v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet:
- // Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name).
- // User können hier per Settings → Tabs einen eigenen Glyph setzen.
+ // Optional FontAwesome glyph name; null falls back to TabIconMapping default.
public string? Icon = null;
[Obsolete("Removed in favor of SelectedChannels")]
@@ -510,15 +401,12 @@ public class Tab
[NonSerialized]
public Guid Identifier = Guid.NewGuid();
- // Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the
- // sidebar to mark a tell partner as already greeted in the current
- // session. NonSerialized because the temp tab itself is session-only.
+ // Session-only greeted flag for club-greeter workflows.
[NonSerialized]
public bool IsGreeted;
- // v1.4.2 — TabTintCache uses separate validation keys per cache so a
- // TellTarget change picked up by GetTint can't strand GetIcon (or vice
- // versa) with a stale entry that looks fresh on the shared key.
+ // Separate validation keys per cache so TellTarget changes don't
+ // cause GetTint and GetIcon to strand each other with stale entries.
[NonSerialized]
internal string? _cachedTintTellName;
@@ -540,17 +428,12 @@ public class Tab
public bool Matches(Message message)
{
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
- {
return false;
- }
- // Auto-tell temp tabs are bound to a single conversation partner;
- // every other tell that matches the channel filter must NOT land
- // here, otherwise all temp tabs would mirror "Tell Exclusive".
+ // Temp tabs are bound to a single conversation partner — other tells
+ // matching the channel filter must not land here.
if (IsTempTab && TellTarget?.IsSet() == true)
- {
return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
- }
return true;
}
@@ -610,10 +493,7 @@ public class Tab
};
}
- ///
- /// MessageList provides an ordered list of messages with duplicate ID
- /// tracking, sorting and mutex protection.
- ///
+ /// Ordered message list with duplicate ID tracking, sorting and mutex protection.
public class MessageList
{
private readonly SemaphoreSlim LockSlim = new(1, 1);
@@ -701,10 +581,7 @@ public class Tab
}
}
- ///
- /// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read
- /// ist OK für 1×/sec Status-Bar-Polling (v1.2.0).
- ///
+ /// Current message count. Lock-per-read is acceptable for 1×/sec status bar polling.
public int Count
{
get
@@ -721,9 +598,7 @@ public class Tab
}
}
- ///
- /// Returns an array copy of the message list for usage outside of main thread
- ///
+ /// Returns an array copy of the message list for usage outside of main thread.
public async Task GetCopy(int millisecondsTimeout = -1)
{
await LockSlim.WaitAsync(millisecondsTimeout);
@@ -737,10 +612,7 @@ public class Tab
}
}
- ///
- /// GetReadOnly returns a read-only list of messages while holding a
- /// reader lock. The list should be used with a using statement.
- ///
+ /// Returns a read-only list while holding a reader lock. Use with a using statement.
public RLockedMessageList GetReadOnly(int millisecondsTimeout = -1)
{
LockSlim.Wait(millisecondsTimeout);
diff --git a/HellionChat/EmoteCache.cs b/HellionChat/EmoteCache.cs
index e47143d..320006f 100644
--- a/HellionChat/EmoteCache.cs
+++ b/HellionChat/EmoteCache.cs
@@ -79,7 +79,7 @@ public static class EmoteCache
Done,
}
- // All of this data is uninitalized while State is not `LoadingState.Done`
+ // All fields below are uninitialised while State != Done.
public static LoadingState State = LoadingState.Unloaded;
private static readonly Dictionary Cache = new();
@@ -87,15 +87,11 @@ public static class EmoteCache
public static string[] SortedCodeArray = [];
- // Plugin-scoped cancellation source for in-flight emote loads. Dispose
- // cancels every running download/texture-create so the workers don't
- // touch a torn-down TextureProvider on plugin reload. Replaced with a
- // fresh source on the next LoadData() call so a re-enable still works.
+ // Cancelled on Dispose to stop in-flight downloads; replaced on re-enable.
private static CancellationTokenSource Cts = new();
internal static CancellationToken Token => Cts.Token;
- // Drain target for in-flight loads on Dispose; without this an orphan
- // continuation could still write to a torn-down Texture/Frames field.
+ // Tracks in-flight loads so Dispose can drain them before teardown.
private static readonly ConcurrentBag PendingLoads = new();
internal static void TrackLoad(Task loadTask, string emoteCode)
@@ -117,8 +113,7 @@ public static class EmoteCache
if (State is not LoadingState.Unloaded)
return;
- // Refresh the CTS in case Dispose was called and we're being re-enabled
- // in the same process (Dalamud /xlplugins toggle).
+ // Reset CTS if Dispose was called and the plugin is being re-enabled.
if (Cts.IsCancellationRequested)
Cts = new CancellationTokenSource();
@@ -140,11 +135,8 @@ public static class EmoteCache
var topList = await top.Content.ReadAsStringAsync(ct);
var jsonList = JsonSerializer.Deserialize>(topList)!;
- // BetterTTV occasionally returns entries with a null Code; the
- // upstream code passed those straight into Dictionary.TryAdd
- // and tripped ArgumentNullException, killing the whole emote
- // load. Skip them defensively so a single bad row no longer
- // breaks the cache for everyone else.
+ // BetterTTV occasionally returns entries with a null Code;
+ // skip them so a single bad row doesn't break the whole cache.
foreach (var emote in jsonList)
if (
!string.IsNullOrEmpty(emote.Emote.Code)
@@ -160,16 +152,11 @@ public static class EmoteCache
}
catch (OperationCanceledException)
{
- // Plugin disposed while the cache was loading; leave State on
- // Loading so a subsequent re-enable can re-issue LoadData with
- // a fresh CTS (handled above).
+ // Plugin disposed mid-load; State stays on Loading so re-enable can retry.
}
catch (Exception ex)
{
- // Reset to Unloaded so a later trigger (e.g. the user reopening
- // the Emotes tab after the network recovers) can retry. Without
- // this the State stays on Loading and the early-out at the top
- // of LoadData blocks every further attempt until plugin reload.
+ // Reset to Unloaded so a later trigger can retry without a plugin reload.
State = LoadingState.Unloaded;
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
}
@@ -248,11 +235,8 @@ public static class EmoteCache
internal async Task LoadAsync(Emote emote, CancellationToken ct)
{
- // BetterTTV-supplied Id and ImageType are interpolated straight
- // into the filename. HTTPS protects the wire, but a compromised
- // upstream could still hand us "../foo" and write into the
- // pluginConfigs root (or worse). Resolve the candidate path and
- // refuse anything that escapes the cache directory.
+ // Path-traversal guard: resolve and verify the candidate path stays
+ // inside the cache directory before reading or writing.
var dir = Path.GetFullPath(
Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1")
);
@@ -397,7 +381,7 @@ public static class EmoteCache
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
- // Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
+ // Match browser behaviour: anything under 20ms rounds up to 100ms.
if (delay < 0.02f)
delay = 0.1f;
@@ -416,8 +400,7 @@ public static class EmoteCache
}
catch (OperationCanceledException)
{
- // Plugin disposed mid-load; partial frames are released by
- // InnerDispose on the next dispose pass.
+ // Plugin disposed mid-load; release any partial frames.
foreach (var f in Frames)
f.Texture.Dispose();
Frames = [];
diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs
index 80d0535..6a86efa 100644
--- a/HellionChat/FontManager.cs
+++ b/HellionChat/FontManager.cs
@@ -41,12 +41,7 @@ public class FontManager
90f,
];
- ///
- /// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
- /// extracted from the assembly's manifest resources on first use; the
- /// load happens inside the font atlas build callback so we keep the
- /// allocation off the plugin constructor's hot path.
- ///
+ // Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
private static byte[]? HellionFontBytes;
private static byte[] GetHellionFontBytes()
@@ -70,11 +65,9 @@ public class FontManager
ushort[] BuildRange(IReadOnlyList? chars, params nint[] ranges)
{
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
- // text
foreach (var range in ranges)
builder.AddRanges((ushort*)range);
- // chars
if (chars != null)
{
for (var i = 0; i < chars.Count; i += 2)
@@ -116,13 +109,7 @@ public class FontManager
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
}
- ///
- /// Async wrapper around for the Phase-1 LoadAsync
- /// path. The font-atlas build is CPU-bound, so we offload via Task.Run and
- /// honour the cancellation token at the scheduling boundary; this lets the
- /// font build run in parallel with the theme init without blocking the
- /// loader. Settings-driven manual rebuilds keep using the sync entry point.
- ///
+ // CPU-bound build offloaded to Task.Run; runs parallel with theme init
public async Task BuildFontsAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -154,12 +141,7 @@ public class FontManager
RegularFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
e.OnPreBuild(tk =>
{
- // v1.2.0 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font)
- // wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet.
- // Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
- // Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei
- // UseHellionFont=true wirkungslos, was 4K-User mit größerer
- // Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten).
+ // v1.2.0: UseHellionFont controls font size selection
var basePt = Plugin.Config.UseHellionFont
? Plugin.Config.FontSizeV2
: Plugin.Config.GlobalFontV2.SizePt;
@@ -218,13 +200,7 @@ public class FontManager
}
}
- ///
- /// Try to add a user-configured font to the build toolkit, falling back to
- /// the bundled NotoSansCjkRegular asset if the configured font isn't
- /// available on the system. Without this guard a stale SystemFontId
- /// pointing at a font the user uninstalled or that never existed on
- /// Linux (e.g. "Crimson Text") tears down the entire font atlas build.
- ///
+ // Add font with fallback to NotoSansCjkRegular if unavailable
private static ImFontPtr AddFontWithFallback(
IFontAtlasBuildToolkitPreBuild tk,
IFontId fontId,
diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj
index 50d8d5d..67d73d1 100644
--- a/HellionChat/HellionChat.csproj
+++ b/HellionChat/HellionChat.csproj
@@ -1,36 +1,21 @@
-
+
1.4.3
enable
enable
-
+
true
-
+
HellionChat
HellionChat
-
+
-
+
@@ -38,9 +23,7 @@
-
+
@@ -59,15 +42,7 @@
-
-
-
+
HellionFont.ttf
@@ -80,14 +55,7 @@
-
+
PreserveNewest
diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml
index fe6b000..2e41e34 100755
--- a/HellionChat/HellionChat.yaml
+++ b/HellionChat/HellionChat.yaml
@@ -31,26 +31,6 @@ description: |-
- Independent plugin state — own config file and database directory,
so Hellion Chat does not share state with upstream Chat 2
- v1.3.0 First plugin integration cycle. Honorific custom titles
- are shown in the chat header above the message log, with auto-detect
- and silent fallback when Honorific is not installed.
-
- v1.4.0 — Critical Lifecycle Fixes. Plugin reload and shutdown
- are cleaner: SQLite no longer leans on GC pressure to release
- its file, worker threads are explicitly background, deferred
- config saves no longer get lost mid-disable, and pre-v13 config
- backups carry the user's custom theme opacity into the v14 schema
- instead of falling back to the default.
-
- v1.4.1 — Theme Engine Performance plus a tenth built-in.
- HellionStyle.PushGlobal reads pre-computed ABGR values from a
- per-theme cache instead of converting RGBA per slot per frame
- (~13 % render-time recovery in typical scenes). Custom-theme
- hot-reload survives transient file locks (editor mid-save
- keeps the last-known-good snapshot). Synthwave Sunset joins
- as the tenth built-in theme — Hot Magenta + Cyan on midnight
- violet, 80s neon-grid vibes.
-
v1.4.2 — ChatLog Frame-Hot-Path. Three per-frame allocation
patterns gone from the chat-log render path: card-mode borders
hoist invariants out of the per-message loop, auto-tell tab
@@ -184,38 +164,6 @@ changelog: |-
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
- **Hellion Chat 1.4.0 — Critical Lifecycle Fixes**
-
- First sub-patch of the v1.4.x Polish Sweep series. Seven
- known lifecycle and race bugs eliminated before any
- performance refactor sits on top.
-
- - MessageStore disposal no longer triggers GC.Collect
- globally; Pooling=false on the SQLite connection means
- there's nothing left to clean up by hand
- - PendingMessage and RetentionSweep worker threads are
- explicitly marked IsBackground=true so the plugin domain
- can unload during XIVLauncher reload without waiting
- for them
- - EmoteCache image and gif loaders moved from async-void
- to async Task with a shared task tracker, draining
- on Dispose so an in-flight load can no longer write
- to a disposed EmoteImages entry
- - DisposeAsync 10s timeout now warns loudly instead of
- silently leaving the worker behind
- - Plugin.Dispose flushes any pending DeferredSaveFrames
- before tearing services down, so settings changes
- made in the last few frames before disable are no
- longer lost
- - The v13→v14 config migration now reads the pre-v13
- backup and carries HellionThemeWindowOpacity into the
- new WindowOpacity field instead of falling back to
- the default 0.85
-
- Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
-
- Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
-
---
Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
diff --git a/HellionChat/InputHistoryService.cs b/HellionChat/InputHistoryService.cs
index cc653db..d7d9300 100644
--- a/HellionChat/InputHistoryService.cs
+++ b/HellionChat/InputHistoryService.cs
@@ -2,14 +2,8 @@ using System.Collections.Generic;
namespace HellionChat;
-// Hellion Chat — v0.6.0 shared input history. Replaces the embedded
-// ChatLogWindow.InputBacklog so that pop-out windows with their own
-// ChatInputBar can navigate the same Up/Down history as the main window.
-// Index semantics are kept identical to the v0.5.x InputBacklog:
-// index 0 = oldest entry
-// index Count - 1 = newest entry
-// Push performs move-to-newest deduplication: existing entries are
-// removed before the new one is appended at the end.
+// Shared input history for all ChatInputBars (main and pop-out windows).
+// Push deduplicates: existing entries are moved to the end when re-added.
public static class InputHistoryService
{
private const int MaxSize = 30;
@@ -26,8 +20,7 @@ public static class InputHistoryService
var trimmed = entry.Trim();
- // Move-to-newest: existing entries are removed before the append
- // so the same line typed twice does not occupy two history slots.
+ // Move-to-newest: remove existing entry before adding at the end
for (var i = 0; i < _entries.Count; i++)
{
if (_entries[i] == trimmed)
diff --git a/HellionChat/Integrations/HonorificService.cs b/HellionChat/Integrations/HonorificService.cs
index a251560..a4467af 100644
--- a/HellionChat/Integrations/HonorificService.cs
+++ b/HellionChat/Integrations/HonorificService.cs
@@ -6,25 +6,17 @@ using Newtonsoft.Json;
namespace HellionChat.Integrations;
-// We pull Newtonsoft.Json into this single file for IPC compatibility:
-// Honorific serialises its TitleData with Newtonsoft (see
-// Honorific-master/IpcProvider.cs:9 and CustomTitle.cs:12). Using the
-// same library guarantees identical handling of System.Numerics.Vector3?
-// and the enum fields we ignore. Newtonsoft is a transitive dependency
-// via Dalamud, so no new NuGet entry is needed. The rest of HellionChat
-// keeps using System.Text.Json.
+// Newtonsoft.Json is used here for IPC compatibility with Honorific, which
+// serialises TitleData with it. It's a transitive Dalamud dependency — no
+// new NuGet entry needed. The rest of HellionChat uses System.Text.Json.
internal sealed class HonorificService : IDisposable
{
private const string IpcNamespace = "Honorific";
- // Major version of the Honorific IPC contract HellionChat is built against.
- // Used both by the runtime compatibility check and by the settings tab when
- // it tells the user which major version we expected, so the literal lives
- // in exactly one place.
+ // Major version of the Honorific IPC contract we're built against.
internal const uint ExpectedApiMajor = 3;
- // IPC gates we subscribe to. Keep them as fields so Dispose can
- // unsubscribe the same instances we subscribed in the constructor.
+ // IPC gates — kept as fields so Dispose can unsubscribe the same instances.
private readonly ICallGateSubscriber<(uint, uint)> _apiVersion;
private readonly ICallGateSubscriber _getLocalCharacterTitle;
private readonly ICallGateSubscriber _localCharacterTitleChanged;
@@ -48,23 +40,11 @@ internal sealed class HonorificService : IDisposable
_framework = framework;
_log = log;
- // Dalamud caches gate objects per-name for the lifetime of the
- // plugin interface, so we can register subscribers even when
- // Honorific isn't loaded yet — the gate just won't fire. Calling
- // InvokeFunc before Honorific is up will throw, which is why the
- // initial pull below is wrapped in try-catch.
- //
- // Thread-context: plugin constructors run on Dalamud's plugin-loader
- // thread, NOT the framework thread. Honorific's IPC handlers read
- // ObjectTable.LocalPlayer (Honorific IpcProvider.cs:61), which throws
- // "Not on main thread!" outside the framework thread. If Honorific is
- // already loaded when HellionChat starts, a synchronous InvokeFunc
- // here would surface that exception, the broad catch below would
- // mark IsAvailable=false, and OnTitleChanged's `if (!IsAvailable)`
- // gate would block every subsequent title update. We therefore
- // schedule the initial pull onto the framework thread via
- // IFramework.RunOnFrameworkThread so the IPC call sees the right
- // thread context.
+ // Gate objects are cached per-name by Dalamud and safe to register
+ // before Honorific loads — they just won't fire until it does.
+ // Initial pull is scheduled on the framework thread because plugin
+ // constructors run on the loader thread, and Honorific's IPC handlers
+ // read ObjectTable.LocalPlayer which throws off the framework thread.
_apiVersion = pluginInterface.GetIpcSubscriber<(uint, uint)>($"{IpcNamespace}.ApiVersion");
_getLocalCharacterTitle = pluginInterface.GetIpcSubscriber(
$"{IpcNamespace}.GetLocalCharacterTitle"
@@ -84,11 +64,8 @@ internal sealed class HonorificService : IDisposable
public void Dispose()
{
- // Honorific may already be gone by the time we dispose. Wrap each
- // unsubscribe so a missing gate doesn't prevent the others from
- // unsubscribing — leaking even one subscription leaves a callback
- // alive that captures `this`, which keeps the whole service alive
- // and breaks plugin reload.
+ // Wrap each unsubscribe — a missing gate must not block the others.
+ // Leaking a subscription keeps this service alive across plugin reloads.
TryUnsubscribe(() => _localCharacterTitleChanged.Unsubscribe(OnTitleChanged));
TryUnsubscribe(() => _ready.Unsubscribe(OnReady));
TryUnsubscribe(() => _disposing.Unsubscribe(OnDisposing));
@@ -119,34 +96,21 @@ internal sealed class HonorificService : IDisposable
IsAvailable = true;
_versionWarningLogged = false;
- // Pull the current title once at startup; from here on we rely
- // on LocalCharacterTitleChanged events.
var json = _getLocalCharacterTitle.InvokeFunc();
CurrentTitle = ParseTitleJson(json);
}
catch (Exception ex)
{
- // Honorific isn't installed or hasn't initialised yet. The Ready
- // event will give us a second chance later. Log at Debug so
- // users without Honorific don't see noise on every reload.
+ // Honorific not installed or not yet initialised — Ready will retry.
_log.Debug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
IsAvailable = false;
CurrentTitle = null;
}
}
- // Honorific fires LocalCharacterTitleChanged through its nameplate hook
- // (Honorific-master/Plugin.cs:665), which means we get title updates on
- // character switches automatically as soon as the new character is
- // rendered. While the user is in the character-select menu, HellionChat's
- // window is hidden by default via HideWhenNotLoggedIn (Configuration.cs:152),
- // so the stale-title window between logout and login isn't user-visible.
private void OnTitleChanged(string json)
{
- // Don't update cached state when we've already decided we can't trust
- // Honorific (e.g. version mismatch). Subscription stays live in case a
- // compatible Honorific reloads, in which case Ready triggers TryInitialPull
- // and sets IsAvailable back to true.
+ // Skip updates on version mismatch; subscription stays live for reload.
if (!IsAvailable)
return;
CurrentTitle = ParseTitleJson(json);
@@ -154,28 +118,16 @@ internal sealed class HonorificService : IDisposable
private void OnReady()
{
- // Honorific loaded after HellionChat; redo the version check and
- // initial pull. Idempotent on purpose — Honorific can fire Ready
- // more than once across reloads.
- //
- // Honorific's NotifyReady may dispatch from any thread, and
- // TryInitialPull eventually calls IPC handlers that read
- // ObjectTable.LocalPlayer — same "Not on main thread!" hazard as
- // the constructor path. Schedule onto the framework thread.
+ // Schedule on framework thread — NotifyReady can dispatch from any thread.
_framework.RunOnFrameworkThread(TryInitialPull);
}
private void OnDisposing()
{
- // Honorific is unloading. Drop our cached state so the header
- // hides on the next frame; subscriptions stay registered because
- // the gates may come back later (Honorific reload).
- //
- // Race-note: Honorific's NotifyDisposing calls ChangedLocalCharacterTitle(null)
- // BEFORE SendMessage on the Disposing gate (IpcProvider.cs:109-111),
- // so OnTitleChanged is expected to fire first and already null out
- // CurrentTitle. We re-clear here as belt-and-braces; should the
- // ordering ever flip, ShouldRenderSlot would still gate on IsAvailable.
+ // Honorific unloading — clear cached state so the header hides next frame.
+ // Subscriptions stay registered in case Honorific reloads.
+ // CurrentTitle is already nulled by OnTitleChanged before this fires,
+ // re-clearing here is belt-and-braces.
CurrentTitle = null;
IsAvailable = false;
DetectedApiVersion = null;
@@ -193,28 +145,15 @@ internal sealed class HonorificService : IDisposable
}
}
- // Threading note: Dalamud fires IPC events on the framework thread and
- // ImGui renders on the framework thread, so OnTitleChanged and the
- // render path that reads CurrentTitle never race — OnTitleChanged is
- // safe to keep direct (no RunOnFrameworkThread wrap needed) because
- // LocalCharacterTitleChanged delivery is framework-thread by Dalamud
- // contract. If a future change moves either side onto a worker thread,
- // switch to volatile/Interlocked for the CurrentTitle field.
+ // 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.
//
- // The constructor's initial pull and OnReady, on the other hand, are
- // explicitly scheduled via IFramework.RunOnFrameworkThread because
- // they run outside that contract: the constructor executes on the
- // plugin-loader thread, and Honorific's NotifyReady can dispatch from
- // any thread. Both call paths eventually invoke IPC handlers that read
- // ObjectTable.LocalPlayer, which throws "Not on main thread!" off the
- // framework thread — see the constructor comment block for context.
- //
- // Divergence from ChatTwo/Ipc/ExtraChat.cs: that file uses `volatile`
- // on its state fields out of caution. We don't, because the framework-
- // thread delivery is the documented Dalamud contract. If the two files
- // ever need to share a threading audit, this is the place to revisit.
+ // 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 below; tested via HellionChat.Tests/Integrations. ---
+ // --- Pure-logic helpers; tested via HellionChat.Tests/Integrations. ---
internal static HonorificTitleData? ParseTitleJson(string json)
{
diff --git a/HellionChat/Integrations/HonorificTitleData.cs b/HellionChat/Integrations/HonorificTitleData.cs
index 3f67e32..5363851 100644
--- a/HellionChat/Integrations/HonorificTitleData.cs
+++ b/HellionChat/Integrations/HonorificTitleData.cs
@@ -2,13 +2,9 @@ using System.Numerics;
namespace HellionChat.Integrations;
-// Local DTO mirroring Honorific's TitleData shape. We replicate the structure
-// instead of referencing Honorific.dll because a hard build-time dependency
-// would couple the two assemblies and break HellionChat at load time when
-// Honorific is missing. Glow, Color3, GradientColourSet and GradientAnimationStyle
-// are intentionally omitted — Cycle 1 renders text in the primary Color only;
-// the "Honorific Full Fidelity" backlog item adds them later as a pure
-// extension that won't break this DTO's existing consumers.
+// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
+// so HellionChat loads cleanly when Honorific is absent.
+// Glow/gradient fields omitted; Cycle 1 renders primary Color only.
internal sealed record HonorificTitleData(
string? Title,
bool IsPrefix,
diff --git a/HellionChat/Integrations/IntegrationLinks.cs b/HellionChat/Integrations/IntegrationLinks.cs
index fd21bc2..84253a7 100644
--- a/HellionChat/Integrations/IntegrationLinks.cs
+++ b/HellionChat/Integrations/IntegrationLinks.cs
@@ -1,10 +1,6 @@
namespace HellionChat.Integrations;
-// External URLs for the third-party plugins HellionChat integrates with.
-// Kept separate from BrandingLinks (which is for Hellion-owned URLs) so
-// future cycles can extend this file with maintainer attribution links
-// for Moodles, NotificationMaster, ExtraChat, etc. without polluting the
-// brand-links class.
+// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
internal static class IntegrationLinks
{
public const string HonorificRepo = "https://github.com/Caraxi/Honorific";
diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs
index f64f13d..7d74ede 100644
--- a/HellionChat/MessageManager.cs
+++ b/HellionChat/MessageManager.cs
@@ -27,16 +27,7 @@ internal class MessageManager : IAsyncDisposable
private Dictionary Formats { get; } = [];
private ulong LastContentId { get; set; }
- // Messages go into the PendingSync queue first, which will be consumed one
- // at a time in the main thread. This is to delay the async processing until
- // after we've received the content ID from the ContentIdResolver hook.
- //
- // After that, the message is enqueued in the PendingAsync queue, which will
- // be consumed in a separate thread and perform more processing (emotes,
- // URLs) as well as inserting the message into the database.
- // LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last
- // every hook call. Queue.Last() is the LINQ extension and walks the
- // whole queue (O(n)); LinkedList.Last is an O(1) node reference.
+ // PendingSync (main thread) → PendingAsync (worker thread); LinkedList for O(1) Last access
private LinkedList PendingSync { get; } = [];
private ConcurrentQueue PendingAsync { get; } = [];
private readonly Thread PendingMessageThread;
@@ -53,11 +44,8 @@ internal class MessageManager : IAsyncDisposable
}
}
- // Hellion Chat — Auto-Tell-Tabs hook. Fires after a fully processed
- // message has been routed to all matching persistent tabs and stored
- // in the database. The AutoTellTabsService subscribes to spawn or
- // refresh temp tabs without having to wedge itself into ProcessMessage
- // directly.
+ // Auto-Tell-Tabs hook: fires after a message is processed and stored, allowing
+ // AutoTellTabsService to spawn or refresh temp tabs without coupling.
public event Action? MessageProcessed;
internal unsafe MessageManager(Plugin plugin)
@@ -66,8 +54,6 @@ internal class MessageManager : IAsyncDisposable
Store = new MessageStore(DatabasePath());
- // IsBackground so a stuck worker never blocks plugin unload.
- // Cooperative cancel via PendingThreadCancellationToken first, background flag is the safety net.
PendingMessageThread = new Thread(() =>
ProcessPendingMessages(PendingThreadCancellationToken.Token)
)
@@ -107,12 +93,9 @@ internal class MessageManager : IAsyncDisposable
if (PendingMessageThread.IsAlive)
Plugin.Log.Warning(
"PendingMessageThread did not observe cancellation within 10s. "
- + "Worker remains on a background thread; next plugin reload releases it. "
- + "If this recurs, file a bug with /xllog after the previous reload."
+ + "Worker remains on background thread; next plugin reload releases it."
);
- // CTS owns an unmanaged WaitHandle; dispose even if the worker is
- // alive — it checks IsCancellationRequested via the linked token.
PendingThreadCancellationToken.Dispose();
Store.Dispose();
@@ -166,12 +149,7 @@ internal class MessageManager : IAsyncDisposable
internal void ClearAllTabs()
{
- // Hellion Chat — TempTabs haben keine DB-Persistenz (session-only,
- // direkt vom AutoTellTabsService befüllt). Ein Clear+Refilter würde
- // sie leer hinterlassen weil FilterAllTabs nichts aus der DB
- // findet — Tells sind oft durch Privacy-Filter blockiert oder
- // schlicht session-flüchtig. TempTabs vom Clear-Pfad ausschließen
- // damit Settings-Save den Tell-Verlauf nicht zerstört.
+ // TempTabs are session-only (not persisted); exclude them to preserve Tell history
foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
tab.Clear();
}
@@ -184,12 +162,7 @@ internal class MessageManager : IAsyncDisposable
using var messages = Store.GetMostRecentMessages(CurrentContentId, since);
- // We store the pending messages to be added to the chat log in a
- // temporary list, and apply them all at once after filtering.
- // TempTabs werden ausgeschlossen — sie bleiben live-state aus dem
- // AutoTellTabsService, ein DB-Refilter würde sie nur partial
- // wiederherstellen falls Tells in DB liegen, oder leer lassen wenn
- // Privacy-Filter sie blockiert hat.
+ // TempTabs are excluded; they maintain live state from AutoTellTabsService
var pendingTabs = Plugin
.Config.Tabs.Where(t => !t.IsTempTab)
.Select(tab => (tab, new List()))
@@ -198,7 +171,7 @@ internal class MessageManager : IAsyncDisposable
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
pendingMessages.Add(message);
- // Apply the messages to the chat log in one go.
+ // Apply messages to chat log all at once.
foreach (var (tab, pendingMessages) in pendingTabs)
tab.Messages.AddSortPrune(pendingMessages, MessageDisplayLimit);
@@ -207,8 +180,7 @@ internal class MessageManager : IAsyncDisposable
WrapperUtil.AddNotification(Language.LoadMessages_Error, NotificationType.Error);
- // Mark the failed messages as deleted so we don't try to load them
- // again.
+ // Mark failed messages as deleted to prevent retry attempts
var failedIds = messages.FailedMessageIds();
Plugin.Log.Info($"Marking {failedIds.Count} messages as deleted due to parse failures");
foreach (var msgId in messages.FailedMessageIds())
@@ -256,16 +228,10 @@ internal class MessageManager : IAsyncDisposable
// Update colour codes.
GlobalParametersCache.Refresh();
- // We delay messages to be handed off to the async processing thread
- // in the next tick, otherwise we can't get the content ID from the hook
- // below.
+ // Delay to next tick to get content ID from ContentIdResolver hook
PendingSync.AddLast(pendingMessage);
}
- // This hook is called immediately after receiving a message with the
- // message's content ID. If multiple messages are received in the same tick,
- // this will be called for each message immediately after ChatMessage is
- // called for each message.
private unsafe void ContentIdResolver(
RaptureLogModule* agent,
ulong contentId,
@@ -408,7 +374,7 @@ internal class MessageManager : IAsyncDisposable
var after = formats
.GetRange(firstStringParam + 1, secondStringParam - firstStringParam)
.Where(payload => payload.Type == ReadOnlySePayloadType.Text)
- .Select(text => Encoding.UTF8.GetString(text.Body.Span)); // Can't use `ToString()` as it defaults to macro
+ .Select(text => Encoding.UTF8.GetString(text.Body.Span));
var nameFormatting = NameFormatting.Of(string.Join("", before), string.Join("", after));
Formats[type] = nameFormatting;
diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs
index 19d9cf2..a8411f5 100755
--- a/HellionChat/Plugin.cs
+++ b/HellionChat/Plugin.cs
@@ -90,10 +90,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
public readonly WindowSystem WindowSystem = new(PluginName);
- // v1.4.3: properties moved from { get; } to { get; private set; } = null!;
- // because LoadAsync now owns construction of the Phase-2 services.
- // Phase-1 services use the same shape for consistency, even though
- // they're still allocated in the ctor.
+ // Phase-2 services are constructed in LoadAsync; null! shape is kept
+ // consistent across all properties for clarity.
public SettingsWindow SettingsWindow { get; private set; } = null!;
public ChatLogWindow ChatLogWindow { get; private set; } = null!;
public DbViewer DbViewer { get; private set; } = null!;
@@ -115,27 +113,20 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal Ui.StatusBar StatusBar { get; private set; } = null!;
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
- // (B3) Lightless idempotency guard — Dalamud may fire DisposeAsync twice
- // in a reload race; second call short-circuits.
+ // Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
private int _disposeStarted;
internal int DeferredSaveFrames = -1;
- // Serialises retention sweeps. The 24h auto-sweep on plugin load and
- // the manual button in the Privacy tab both run on background threads;
- // without this gate, hitting the manual button moments after a fresh
- // plugin start would launch two sweeps in parallel and the second one
- // would just re-do work the first one already finished. The lock guards
- // the flag — the flag check itself bails before we touch the database.
- // Volatile because the ImGui thread reads the flag outside the lock to
- // gate the manual button; without it the JIT may cache the value in a
- // register and miss the background-thread update.
+ // Serialises retention sweeps so a manual trigger and the 24h auto-sweep
+ // can't run in parallel. Volatile because the ImGui thread reads it outside
+ // the lock to gate the manual button.
internal readonly object RetentionSweepLock = new();
internal volatile bool RetentionSweepRunning;
internal DateTime GameStarted { get; }
- // Tab management needs to happen outside the chatlog window class for access reasons
+ // Tab management lives here rather than in ChatLogWindow for access reasons.
internal int LastTab { get; set; }
internal int? WantedTab { get; set; }
internal Tab CurrentTab
@@ -149,31 +140,22 @@ public sealed class Plugin : IAsyncDalamudPlugin
public Plugin()
{
- // Phase-1 ctor stays minimal: bootstrap-essentials only (conflict
- // gate, config load, language + ImGui init, WindowSystem skeleton).
- // Schema migrations and every service / window allocation moved to
- // LoadAsync so the sync ctor returns fast. On failure here nothing
- // is initialized yet, so just throw — there is nothing to clean up.
+ // Phase-1 ctor: bootstrap-essentials only (conflict gate, config load,
+ // language + ImGui init). All service/window allocation lives in LoadAsync.
- // Refuse to start if upstream Chat 2 is loaded — prevents IPC
- // channel collisions and double-replacement of the in-game chat
- // window. Throwing here makes Dalamud abort the load cleanly with
- // our localized message instead of crashing FFXIV mid-frame.
+ // Block load if upstream Chat 2 is active — prevents IPC collisions
+ // and double-replacement of the in-game chat window.
ChatTwoConflictDetector.ThrowIfChatTwoIsLoaded(Interface);
GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime();
- // Hellion Chat: take over config + database from upstream ChatTwo
- // before Dalamud loads our plugin config. Idempotent: only acts on
- // the first start where the legacy paths exist and ours don't.
+ // Migrate config + database from upstream ChatTwo on first start.
MigrateFromChatTwoLayout();
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
- // Schema-gate: v1.4.3 only supports config schema v16. Older configs
- // went through their migrations in v1.2.1 (v15→v16) and earlier; users
- // who skipped past those releases need to install v1.4.2 first to run
- // the migration chain, then upgrade to v1.4.3.
+ // Schema gate: v1.4.3 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(
@@ -182,19 +164,13 @@ public sealed class Plugin : IAsyncDalamudPlugin
);
}
- // Hellion Chat — Auto-Tell-Tabs Defense-in-Depth. SaveConfig
- // already strips temp tabs before persistence, but a previous
- // crash or external write could have left them in the JSON.
- // Drop them on load to guarantee the session-only invariant.
+ // Drop session-only Auto-Tell-Tabs that a previous crash may have persisted.
Config.Tabs.RemoveAll(t => t.IsTempTab);
LanguageChanged(Interface.UiLanguage);
ImGuiUtil.Initialize(this);
DeferredSaveFrames = -1;
-
- // WindowSystem skeleton is initialised by the readonly field above —
- // no AddWindow yet; window construction lives in LoadAsync.
}
public async Task LoadAsync(CancellationToken cancellationToken)
@@ -203,14 +179,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
try
{
- // Hellion v1.0.0 default tab layout. Five thematically separated
- // tabs: General catches the immediate-surroundings public chat
- // (Say/Yell/Shout) only; System absorbs the rest of the technical
- // and gameplay-event noise; FreeCompany, Group and Linkshell each
- // own their respective channel set. Tells are not in a static
- // tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation
- // tabs on demand. Novice-Network gets no preset tab; users who
- // want it can add HellionBeginner from Settings → Tabs.
+ // Default tab layout on fresh install. Tells are handled by
+ // Auto-Tell-Tabs; Novice Network has no preset tab by design.
if (Config.Tabs.Count == 0)
{
Config.Tabs.Add(TabsUtil.VanillaGeneral);
@@ -222,19 +192,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
cancellationToken.ThrowIfCancellationRequested();
- // Sync allocation + handle registration. BuildFonts() registers
- // IFontHandles with Dalamud's UiBuilder.FontAtlas — registration
- // itself is non-blocking (handles stored, lambdas queued). Dalamud
- // rebuilds the atlas on its own pipeline a few frames later; first
- // frames render with the default font until the rebuild lands and
- // ImGui switches to Hellion-Exo2 / NotoSans (visible "font-pop").
- // Mirrors ChatTwo Plugin.cs:152.
+ // BuildFonts registers handles with Dalamud's FontAtlas; the atlas
+ // rebuilds async a few frames later (visible "font-pop" on first load).
FontManager = new FontManager();
FontManager.BuildFonts();
- // Theme init stays sync on the LoadAsync continuation — cheap,
- // and Active is read every Draw frame, so the registry must be
- // wired before the first hook fires.
+ // ThemeRegistry must be wired before the first Draw tick.
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
Directory.CreateDirectory(customThemesDir);
SeedExampleThemeIfEmpty(customThemesDir);
@@ -243,11 +206,9 @@ public sealed class Plugin : IAsyncDalamudPlugin
cancellationToken.ThrowIfCancellationRequested();
- // Service allocations: order encodes dependencies. Commands is
- // alloc-only here; Initialise() runs after windows exist so the
- // slash-commands can toggle their visibility. HonorificService
- // registers IPC subscribers up-front so Ready/Disposing events
- // are caught from the very first frame.
+ // Service allocations — order encodes dependencies.
+ // HonorificService registers IPC subscribers early to catch
+ // Ready/Disposing events from the first frame.
FileDialogManager = new FileDialogManager();
Commands = new Commands();
Functions = new GameFunctions.GameFunctions(this);
@@ -258,9 +219,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
StatusBar = new Ui.StatusBar();
MessageManager = new MessageManager(this);
- // Auto-Tell-Tabs subscribes to MessageManager.MessageProcessed for
- // live tells and to ClientState.Logout for cleanup; needs the live
- // store handed in at construction.
AutoTellTabsService = new AutoTellTabsService(
this,
MessageManager,
@@ -268,7 +226,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
);
AutoTellTabsService.Initialize();
- // SelfTest steps poll Active per frame and need the registry wired.
SelfTestRegistry.RegisterTestSteps([new SelfTests.ThemeSwitchSelfTestStep(this)]);
ChatLogWindow = new ChatLogWindow(this);
@@ -289,22 +246,19 @@ public sealed class Plugin : IAsyncDalamudPlugin
WindowSystem.AddWindow(DebuggerWindow);
WindowSystem.AddWindow(FirstRunWizard);
- // Open the wizard on a fresh install. Existing ChatTwo users have
- // FirstRunCompleted set to true by the v6→v7 migration above.
if (!Config.FirstRunCompleted)
FirstRunWizard.IsOpen = true;
cancellationToken.ThrowIfCancellationRequested();
- // let all the other components register, then initialize commands
Commands.Initialise();
- // Daily retention sweep, fire-and-forget. Skips itself when
- // disabled or when it already ran within the past 24 hours.
+ // Daily retention sweep — fire-and-forget, skips when disabled
+ // or already ran within the past 24 hours.
RunRetentionSweepIfDue();
if (Config.ShowEmotes)
- _ = EmoteCache.LoadData(); // Fire-and-forget, exceptions caught inside
+ _ = EmoteCache.LoadData();
if (Interface.Reason is not PluginLoadReason.Boot)
MessageManager.FilterAllTabsAsync();
@@ -313,33 +267,22 @@ public sealed class Plugin : IAsyncDalamudPlugin
Interface.UiBuilder.DisableGposeUiHide = true;
#if !DEBUG
- // Fire-and-forget on a worker thread. The first auto-translate use of
- // a session may have a sub-second hitch if the cache hasn't filled yet,
- // but that's preferable to making every user wait ~300 ms during
- // plugin load for a cache they may never touch. ChatTwo (upstream)
- // does this sync; we trade load-time for first-use latency.
+ // Fire-and-forget — first auto-translate use may have a sub-second
+ // hitch if the cache hasn't filled yet, but avoids blocking load.
_ = Task.Run(AutoTranslate.PreloadCache, cancellationToken);
#endif
cancellationToken.ThrowIfCancellationRequested();
- // (B1) Hooks last: every service and window must be live before
- // Dalamud fires our first Draw / FrameworkUpdate tick. Anything
- // earlier risks rendering against null FontManager / ThemeRegistry.
+ // Hooks last — all services and windows must be live before
+ // the first Draw / FrameworkUpdate tick fires.
Framework.Update += FrameworkUpdate;
Interface.UiBuilder.Draw += Draw;
Interface.LanguageChanged += LanguageChanged;
- // Hellion Chat — surface a "main UI" entry point so Dalamud's
- // plugin list shows the Open-Plugin button. Settings is the
- // most useful landing place; OpenConfigUi is already wired to
- // the same toggle inside SettingsWindow.
Interface.UiBuilder.OpenMainUi += OpenMainUi;
}
catch
{
- // Mirror the v1.4.0 load-failure recovery: hand off to DisposeAsync
- // so partially-built services are torn down. Swallow the cleanup
- // exception so the original load failure stays the visible cause.
try
{
await DisposeAsync().ConfigureAwait(false);
@@ -351,28 +294,22 @@ public sealed class Plugin : IAsyncDalamudPlugin
}
}
- // Suppressing this warning because DisposeAsync may run after a partial
- // LoadAsync, so some properties may not be initialized.
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract")]
public async ValueTask DisposeAsync()
{
- // (B3) Idempotency guard — Dalamud may reload-race us; second
- // call short-circuits so we don't double-dispose services.
+ // Idempotency guard — second call short-circuits on reload race.
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
return;
Exception? failure = null;
- // Hooks unsubscribe FIRST so no Draw / FrameworkUpdate / LanguageChanged
- // tick can fire while we're tearing services down. Mirrors the
- // hooks-last subscribe order in LoadAsync.
+ // Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync.
failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi);
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
- // v1.4.0 F5.3 — flush a pending DeferredSave before service teardown,
- // since FrameworkUpdate just got unsubscribed and won't fire it.
+ // Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore.
failure = CaptureFailure(
failure,
() =>
@@ -385,13 +322,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
}
);
- // Auto-Tell-Tabs unsubscribes from MessageProcessed before MessageManager
- // goes away. Pure-memory cleanup, no framework-thread requirement.
+ // Unsubscribe AutoTellTabs before MessageManager goes away.
failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose());
- // v1.4.0 F6.2 — MessageManager has its own async dispose path
- // (DB flush, pending-message thread shutdown). Run it before the
- // framework-block so the worker threads are quiesced first.
+ // MessageManager has its own async dispose path (DB flush, thread shutdown).
if (MessageManager is not null)
{
failure = await CaptureFailureAsync(
@@ -401,36 +335,24 @@ public sealed class Plugin : IAsyncDalamudPlugin
.ConfigureAwait(false);
}
- // (B4) Game-Function / IPC / UI-Window cleanup MUST run on the
- // framework thread. WindowSystem mutations and IPC subscriber
- // disposes touch Dalamud state that's only safe from the framework.
- // Worker-thread DisposeAsync would race the next Draw tick.
- // Per-line CaptureFailure so a single throw can't strand the lines
- // behind it; mirrors Lightless DisposeFrameworkBoundServicesAsync.
+ // Game-function / IPC / window cleanup must run on the framework thread.
try
{
await Framework
.RunOnFrameworkThread(() =>
{
- // Game-Functions first — other services may still query
- // chat-interactable state during their Dispose.
failure = CaptureFailure(
failure,
() => GameFunctions.GameFunctions.SetChatInteractable(true)
);
- // IPC subscribers — dispose before windows so any final
- // event firing from the IPC source can't reach a half-torn
- // ChatLogWindow.
+ // IPC subscribers before windows — prevents a final IPC event
+ // from reaching a half-torn ChatLogWindow.
failure = CaptureFailure(failure, () => HonorificService?.Dispose());
failure = CaptureFailure(failure, () => TypingIpc?.Dispose());
failure = CaptureFailure(failure, () => ExtraChat?.Dispose());
failure = CaptureFailure(failure, () => Ipc?.Dispose());
- // Windows — RemoveAllWindows first, then per-window Dispose.
- // Order matches the pre-v1.4.3 Dispose body byte-for-byte.
- // CommandHelpWindow and FirstRunWizard don't implement
- // IDisposable; their resources are reclaimed via WindowSystem.
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose());
failure = CaptureFailure(failure, () => DbViewer?.Dispose());
@@ -446,8 +368,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
failure ??= ex;
}
- // Pure-memory cleanups — no Framework / UI / IPC touch, so they
- // run on whatever thread DisposeAsync resumes on.
+ // Pure-memory cleanups — no Framework / UI / IPC touch.
failure = CaptureFailure(failure, () => Functions?.Dispose());
failure = CaptureFailure(failure, () => Commands?.Dispose());
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
@@ -456,9 +377,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
ExceptionDispatchInfo.Capture(failure).Throw();
}
- // Lightless-pattern capture helpers: run cleanup, remember the FIRST
- // exception, keep going. Without these one mid-teardown failure would
- // skip every cleanup behind it and leave services half-torn.
+ // Run cleanup actions individually so a single failure doesn't strand
+ // the remaining teardown steps.
private static Exception? CaptureFailure(Exception? failure, Action action)
{
try
@@ -499,9 +419,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
var ourConfigFile = Path.Combine(pluginConfigsDir, "HellionChat.json");
var ourConfigDir = Interface.ConfigDirectory.FullName;
- // Track whether anything legitimately blocked us. The most common
- // cause is upstream Chat 2 still being loaded — its SQLite handle
- // keeps chat-sqlite.db locked and File.Move throws IOException.
var lockedBlocker = false;
try
@@ -523,13 +440,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
lockedBlocker = true;
}
- // The plugin's ConfigDirectory may already exist on first load
- // (Dalamud creates it), so check at the file level instead of
- // skipping when the directory is present. Move every legacy
- // entry whose target name is not occupied yet, then remove the
- // source dir if it ends up empty. Each move is wrapped on its
- // own so a single locked file (the SQLite db while ChatTwo still
- // runs) does not abandon the rest of the migration.
if (!Directory.Exists(legacyConfigDir))
return;
@@ -537,6 +447,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
{
Directory.CreateDirectory(ourConfigDir);
+ // Move each file individually so a single locked file (e.g. the
+ // SQLite db while ChatTwo is still loaded) doesn't abort the rest.
foreach (var file in Directory.EnumerateFiles(legacyConfigDir))
{
var target = Path.Combine(ourConfigDir, Path.GetFileName(file));
@@ -590,9 +502,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (lockedBlocker)
{
- // Surface the most common cause to the user as a notification
- // so they don't think Hellion Chat lost their history when in
- // fact upstream Chat 2 was still holding the database file.
Notification.AddNotification(
new Dalamud.Interface.ImGuiNotification.Notification
{
@@ -610,10 +519,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
private void OpenMainUi()
{
- // Settings is the most useful landing surface — same target as the
- // Configure button. SettingsWindow.Toggle is internal and already
- // wired to OpenConfigUi, so re-using IsOpen keeps both entry points
- // behaviourally identical.
SettingsWindow.IsOpen = !SettingsWindow.IsOpen;
}
@@ -624,8 +529,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (DateTimeOffset.UtcNow - Config.RetentionLastRunAt < TimeSpan.FromHours(24))
return;
- // Snapshot the policy so the user can edit settings while we run.
- // Spec defaults form the baseline; explicit user overrides win.
+ // Snapshot the policy so the user can edit settings while the sweep runs.
var policy = new Dictionary();
foreach (var (type, days) in Privacy.PrivacyDefaults.DefaultRetentionDays)
policy[(int)(ushort)type] = days;
@@ -633,16 +537,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
policy[(int)(ushort)type] = days;
var defaultDays = Config.RetentionDefaultDays;
- // IsBackground = true for the same reason as PendingMessageThread:
- // a stuck sweep must never block plugin unload. RunRetentionSweepIfDue
- // guards the run-frequency, and the sweep itself uses the framework's
- // cooperative cancellation pattern. The background flag is the safety
- // net if the sweep ever takes longer than expected.
+ // IsBackground = true so a stuck sweep never blocks plugin unload.
new Thread(() =>
{
- // Bail out cheaply if a manual sweep is already in flight; the
- // lock around the actual work would queue us up otherwise and
- // we would just re-do whatever the manual run already did.
+ // Bail early if a manual sweep is already in flight.
lock (RetentionSweepLock)
{
if (RetentionSweepRunning)
@@ -659,11 +557,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (deleted > 0)
{
Log.Information($"Retention sweep deleted {deleted} expired messages.");
- // Run the clear+refilter synchronously on the framework thread.
- // Earlier this called FilterAllTabsAsync(), which is fire-and-forget
- // — the .Wait() here would return as soon as the inner Task.Run was
- // dispatched, racing the next sweep cycle against the still-running
- // filter pass. See AUDIT-2026-05-05 [QUAL-02].
+ // Run clear+refilter on the framework thread — FilterAllTabsAsync
+ // is fire-and-forget and would race the next sweep cycle.
Framework
.Run(() =>
{
@@ -694,9 +589,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
private void Draw()
{
- // Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes
- // Theme statt einem deaktivierten Hellion-Theme. Active wird einmal
- // pro Frame aus der Registry gelesen.
+ // Theme engine is always active; Classic is a theme, not a disabled state.
using IDisposable _style = HellionStyle.PushGlobal(
ThemeRegistry.Active,
Config.WindowOpacity
@@ -711,9 +604,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
return;
}
- // v1.0.2 — global skip while the New Game+ menu (QuestRedo addon) is
- // open. Hides every plugin window in one shot (chat log, pop-outs,
- // settings, db viewer, etc.), matching the LoadingScreens pattern.
+ // Hide all plugin windows while the New Game+ menu is open.
if (
Config.HideInNewGamePlusMenu
&& GameFunctions.GameFunctions.IsAddonInteractable(
@@ -742,10 +633,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal void SaveConfig()
{
- // Hellion Chat — Auto-Tell-Tabs are session-only. Strip them out
- // before serialization so a crash mid-session can never persist
- // them. We snapshot the full tab list first and restore it after
- // the save, preserving the user's order and open conversations.
+ // Strip session-only Auto-Tell-Tabs before serialization; restore after.
var snapshot = Config.Tabs.ToList();
Config.Tabs.RemoveAll(t => t.IsTempTab);
@@ -794,9 +682,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
Condition[ConditionFlag.OccupiedInCutSceneEvent]
|| Condition[ConditionFlag.WatchingCutscene78];
- // v1.1.0 — wenn der themes/-Ordner leer ist, schreiben wir die embedded
- // example-theme.json als Vorlage rein. Bestehende User-Customs werden
- // nicht angefasst (existing JSONs lassen den Block überspringen).
+ // Seeds example-theme.json into the themes dir on first run.
+ // Skipped if any custom JSON already exists.
private static void SeedExampleThemeIfEmpty(string dir)
{
if (Directory.EnumerateFiles(dir, "*.json").Any())
diff --git a/HellionChat/Resources/ChatColourPresets.cs b/HellionChat/Resources/ChatColourPresets.cs
index 9ea4881..4008bdf 100644
--- a/HellionChat/Resources/ChatColourPresets.cs
+++ b/HellionChat/Resources/ChatColourPresets.cs
@@ -4,11 +4,8 @@ using HellionChat.Util;
namespace HellionChat.Resources;
-// Hellion Chat — v0.6.0 built-in colour presets for the ChatColours
-// settings section. Read-only static data; users apply a preset via the
-// settings UI which overwrites Configuration.ChatColours immediately.
-// Battle-channel types are intentionally NOT covered by the stylistic
-// presets so that combat-log tuning the user has done stays intact.
+// Built-in colour presets applied via Settings UI → ChatColours.
+// Battle-channel types are intentionally excluded to preserve combat-log tuning.
public sealed record ChatColourPreset(
string DisplayName,
string LocalizationKey,
@@ -69,9 +66,7 @@ public static class ChatColourPresets
};
}
- // The Default preset spiegelt 1:1 die Werte aus ChatTypeExt.DefaultColor.
- // Channels ohne Default-Wert (return null) werden ausgelassen — wer sie
- // anwenden will, behält seine aktuelle Farbe.
+ // Mirrors ChatTypeExt.DefaultColor; channels without a default are skipped.
private static IReadOnlyDictionary BuildDefault()
{
var dict = new Dictionary();
@@ -183,33 +178,22 @@ public static class ChatColourPresets
};
}
- // Hellion brand preset — Arctic Cyan + Ember Orange palette aus
- // /mnt/ssd-fast/Projekte/hellion-media/hellion-media-website/BRANDING.md
- // (Schema-Stand 2026-04-16). Channels sind über das ganze Brand-Spektrum
- // verteilt damit jede Zeile auf einen Glance unterscheidbar ist:
- // Cyan-Familie für Standard/Tell, Ember + Warning für laute Channels,
- // Status-Farben (Success, Danger) für Linkshells. CrossLinkshells
- // nutzen die dunkleren/sattersten Varianten derselben Hue-Familien.
+ // Hellion brand preset — Arctic Cyan + Ember Orange palette.
+ // Cyan family for Standard/Tell, Ember/Warning for loud channels,
+ // Status colours for Linkshells, darker variants for CrossLinkshells.
private static IReadOnlyDictionary BuildHellion()
{
return new Dictionary
{
- // Standard / Tell — Cyan-Familie (Brand-Primary)
[ChatType.Say] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light #4DD9E8
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan #00BED2
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark #0097A7
-
- // Laute Channels — Ember/Warning
[ChatType.Yell] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning #F0AD4E
[ChatType.Shout] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember #F97316
-
- // Gruppen-Channels — Success/Ember-dark/Cyan
[ChatType.Party] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success #5CB85C
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark #E85D04
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
-
- // Linkshells 1-8 — über das ganze Brand-Spektrum verteilt
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(251, 146, 60), // Ember-light #FB923C
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success
@@ -218,8 +202,6 @@ public static class ChatColourPresets
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(217, 83, 79), // Danger #D9534F
-
- // CrossWorld-Linkshells 1-8 — dunklere/sattersere Varianten
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(200, 140, 50), // Warning-dark
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(60, 140, 60), // Success-dark
@@ -231,31 +213,20 @@ public static class ChatColourPresets
};
}
- // Bonus preset — Night Blue, KAZAMA-Stimmungs-Theme aus
- // /mnt/HDD-Data1/Obsidian/Vault/Systeme/KAZAMA/Theming/Night Blue + Indigo Violet Themes.md
- // Klassisch, kühl, technisch — Marineblau-Tiefe ohne Lila-Anteil.
- // Bewusst NICHT als Brand-Preset markiert (Vault-Boundary): die KAZAMA-Themes
- // sind persönliche Stimmungs-Themes, nicht Teil des Hellion-Brand-Systems.
+ // Night Blue — cool nautical theme, deep navy without purple.
private static IReadOnlyDictionary BuildNightBlue()
{
return new Dictionary
{
- // Standard / Tell — Royal Blue Akzent-Familie
[ChatType.Say] = ColourUtil.ComponentsToRgba(230, 237, 247), // text-primary
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(106, 176, 255), // akzent-hot
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
-
- // Laute Channels — Warning/Danger Status-Töne
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74), // warning
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122), // danger
-
- // Gruppen — Success/Akzent-Variations
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151), // success
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100), // warm-orange-light
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(140, 160, 191), // text-dim
-
- // Linkshells 1-8 — über Spektrum verteilt
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
@@ -264,8 +235,6 @@ public static class ChatColourPresets
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(100, 200, 220),
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(106, 176, 255),
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(140, 160, 191),
-
- // CrossWorld-Linkshells — gedämpfte Variants
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
@@ -277,30 +246,20 @@ public static class ChatColourPresets
};
}
- // Bonus preset — Indigo Violet, KAZAMA-Stimmungs-Theme aus demselben
- // Vault-Doc. Warm-mystisch, "Galaxy/Glitter/Nordlicht" — tiefes Indigo
- // mit kräftigem Violet-Akzent. Persönlicher Favorit (siehe Vault).
- // Auch nicht als Brand-Preset (siehe NightBlue-Note oben).
+ // Indigo Violet — warm-mystic theme, deep indigo with violet accent.
private static IReadOnlyDictionary BuildIndigoViolet()
{
return new Dictionary
{
- // Standard / Tell — Royal Violet Akzent-Familie
- [ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary (light lavender)
+ [ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(176, 124, 255), // akzent-hot
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
-
- // Laute Channels — geteilt mit Night Blue (Status-Farben)
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74),
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122),
-
- // Gruppen
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151),
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100),
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(168, 144, 208), // text-dim
-
- // Linkshells 1-8
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
@@ -309,8 +268,6 @@ public static class ChatColourPresets
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(139, 77, 222),
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(130, 90, 200),
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(168, 144, 208),
-
- // CrossWorld-Linkshells
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
diff --git a/HellionChat/SelfTests/ThemeSwitchSelfTestStep.cs b/HellionChat/SelfTests/ThemeSwitchSelfTestStep.cs
index 3eb6652..7210bad 100644
--- a/HellionChat/SelfTests/ThemeSwitchSelfTestStep.cs
+++ b/HellionChat/SelfTests/ThemeSwitchSelfTestStep.cs
@@ -4,13 +4,9 @@ using HellionChat.Themes;
namespace HellionChat.SelfTests;
-// Validates the runtime theme-switch contract from the user side. The
-// caller toggles the active theme via Settings -> Theme & Layout, the
-// step polls ThemeRegistry.Active per frame and only passes once the
-// slug has moved away from the initial value and back. The ABGR cache
-// is sanity-checked on every frame: a freshly switched theme must carry
-// a populated cache, otherwise Switch() forgot the recompute and the UI
-// would still draw, just with all-transparent slots.
+// Validates the runtime theme-switch contract: polls ThemeRegistry.Active
+// per frame until the slug moves away and back, then sanity-checks that
+// the ABGR cache was recomputed on switch.
internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep
{
private readonly Plugin plugin;
@@ -73,9 +69,8 @@ internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep
this.switchedAway = false;
}
- // Any non-zero slot proves the cache was actually recomputed for the
- // current theme. We don't compare against a reference, because custom
- // themes can legitimately share slot values with a built-in.
+ // Any non-zero slot confirms the cache was recomputed — no reference
+ // comparison since custom themes can share slot values with built-ins.
private static bool HasPopulatedCache(Theme theme)
{
var cache = theme.AbgrCache;
diff --git a/HellionChat/Themes/Builtin/SynthwaveSunset.cs b/HellionChat/Themes/Builtin/SynthwaveSunset.cs
index 3fe4426..6fa0492 100644
--- a/HellionChat/Themes/Builtin/SynthwaveSunset.cs
+++ b/HellionChat/Themes/Builtin/SynthwaveSunset.cs
@@ -10,7 +10,7 @@ internal static class SynthwaveSunset
new(
Slug: Slug,
Name: "Synthwave Sunset",
- Author: "Hellion Forge",
+ Author: "Zoe Moon",
Description: "Hot Magenta + Cyan on midnight violet. 80s neon-grid vibes for late-night raids.",
Colors: new ThemeColors(
PrimaryDark: ColourUtil.HexToRgba("#C71585"),
diff --git a/HellionChat/Themes/ThemeChatColors.cs b/HellionChat/Themes/ThemeChatColors.cs
index c1518b4..0d2ed6a 100644
--- a/HellionChat/Themes/ThemeChatColors.cs
+++ b/HellionChat/Themes/ThemeChatColors.cs
@@ -2,8 +2,6 @@ using HellionChat.Code;
namespace HellionChat.Themes;
-// Optional pro Theme. Wenn ein Theme ChatColors mitliefert, kann der
-// User sie per Klick im Themes-Tab auf Configuration.ChatColours anwenden.
-// Ein Theme ohne ChatColors (z.B. chat2-classic) lässt die User-Channel-
-// Farben unverändert.
+// Optional per-theme chat colours applied to Configuration.ChatColours on user request.
+// Themes without this leave channel colours untouched.
public sealed record ThemeChatColors(IReadOnlyDictionary Channels);
diff --git a/HellionChat/Themes/ThemeColors.cs b/HellionChat/Themes/ThemeColors.cs
index 96f99d4..c131d43 100644
--- a/HellionChat/Themes/ThemeColors.cs
+++ b/HellionChat/Themes/ThemeColors.cs
@@ -1,6 +1,6 @@
namespace HellionChat.Themes;
-// Color-Werte als 0xRRGGBBAA, RgbaToAbgr handled den Byte-Swap zu ImGui.
+// Colour values as 0xRRGGBBAA — RgbaToAbgr handles the byte-swap for ImGui.
public sealed record ThemeColors(
uint PrimaryDark,
uint Primary,
diff --git a/HellionChat/Themes/ThemeJsonLoader.cs b/HellionChat/Themes/ThemeJsonLoader.cs
index 4c53c0e..88a4c32 100644
--- a/HellionChat/Themes/ThemeJsonLoader.cs
+++ b/HellionChat/Themes/ThemeJsonLoader.cs
@@ -66,10 +66,8 @@ internal static class ThemeJsonLoader
var dict = new Dictionary();
foreach (var prop in el.EnumerateObject())
{
- // Property-Name ist der ChatType-Name als String (z.B. "Say", "Tell"),
- // Value ist Hex wie bei den Theme-Colors. Unbekannte Channel-Names
- // werden still übersprungen — Forward-Compat falls SE neue Channels
- // einführt.
+ // Property name is the ChatType name (e.g. "Say", "Tell"), value is hex like theme colours.
+ // Unknown channel names are silently skipped for forward-compat with future SE channels.
if (
!Enum.TryParse(
prop.Name,
diff --git a/HellionChat/Themes/ThemeLayout.cs b/HellionChat/Themes/ThemeLayout.cs
index 116c2a3..d195e91 100644
--- a/HellionChat/Themes/ThemeLayout.cs
+++ b/HellionChat/Themes/ThemeLayout.cs
@@ -1,6 +1,6 @@
namespace HellionChat.Themes;
-// Layout-Werte spiegeln die ImGuiStyleVar-Slots, die HellionStyle pusht.
+// Layout values mirror the ImGuiStyleVar slots pushed by HellionStyle.
public sealed record ThemeLayout(
float WindowRounding,
float ChildRounding,
diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs
index 5ff7407..ac26dd7 100644
--- a/HellionChat/Themes/ThemeRegistry.cs
+++ b/HellionChat/Themes/ThemeRegistry.cs
@@ -29,7 +29,7 @@ public sealed class ThemeRegistry
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
};
- // Centralised so the ten .Build() factories stay free of cache plumbing.
+ // Centralised so Build() factories stay free of cache plumbing.
foreach (var theme in _builtIns.Values)
theme.RecomputeAbgrCache();
@@ -58,14 +58,13 @@ public sealed class ThemeRegistry
public void Switch(string slug)
{
var theme = Get(slug);
- // Defensive — idempotent and cheap, so any future theme source
- // that forgets the cache fill still ends up with a populated one.
+ // Defensive — ensures any future theme source always gets a populated cache.
theme.RecomputeAbgrCache();
_active = theme;
}
- // 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION. Other
- // IO failures are permanent and get the theme dropped instead of retried.
+ // 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION.
+ // Other IO failures are permanent — theme is dropped instead of retried.
internal static bool IsRecoverableFileLock(Exception? ex)
{
if (ex is not IOException io)
@@ -74,9 +73,8 @@ public sealed class ThemeRegistry
return code == 0x80070020u || code == 0x80070021u;
}
- // Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit
- // LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup
- // neu eingelesen.
+ // Custom themes are loaded lazily, cached by LastWriteTime.
+ // A changed JSON is reloaded on the next lookup.
private Theme? LoadCustomBySlug(string slug)
{
if (_customThemesDir is null)
@@ -115,8 +113,7 @@ public sealed class ThemeRegistry
}
catch (Exception ex) when (IsRecoverableFileLock(ex))
{
- // Editor mid-save: keep the cached snapshot, leave the stamp
- // alone so the next refresh retries automatically.
+ // Editor mid-save: keep last known good, retry on next refresh.
Plugin.Log.Debug(
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
);
diff --git a/HellionChat/Themes/ThemeTypography.cs b/HellionChat/Themes/ThemeTypography.cs
index b1fbf3a..9f7a981 100644
--- a/HellionChat/Themes/ThemeTypography.cs
+++ b/HellionChat/Themes/ThemeTypography.cs
@@ -1,7 +1,6 @@
namespace HellionChat.Themes;
-// Optional pro Theme. v1.1.0 nutzt das nicht aktiv; ist als Erweiterungspunkt
-// für zukünftige Theme-Slots vorbereitet.
+// Optional per-theme; reserved as an extension point for future theme slots.
public sealed record ThemeTypography(
float? OverrideGlobalFontSizePt = null,
float? OverrideSymbolsFontSizePt = null