diff --git a/HellionChat/Export/MessageExporter.cs b/HellionChat/Export/MessageExporter.cs
index fbc6b01..c5c02fb 100644
--- a/HellionChat/Export/MessageExporter.cs
+++ b/HellionChat/Export/MessageExporter.cs
@@ -32,12 +32,8 @@ internal static class ExportFormatExt
};
}
-///
-/// Serializes message snapshots into Markdown, JSON, or CSV. The caller is
-/// expected to filter the input enumerable; this class only handles
-/// formatting and writes to the supplied path. Sender substring filtering
-/// happens here because it requires deserialized SeString.TextValue.
-///
+// Serializes message snapshots to Markdown, JSON, or CSV.
+// Caller handles pre-filtering except sender substring, which requires deserialized SeString.TextValue.
internal static class MessageExporter
{
internal record FilterDescription(
@@ -100,6 +96,7 @@ internal static class MessageExporter
var chatType = (ChatType)(ushort)m.Code.Type;
var sender = m.SenderSource.TextValue.Trim().Trim('<', '>', '[', ']', ':').Trim();
var content = m.ContentSource.TextValue;
+
if (string.IsNullOrEmpty(sender))
w.WriteLine($"**[{localDate:HH:mm}] {chatType}:** {content}");
else
@@ -132,8 +129,7 @@ internal static class MessageExporter
FilterDescription filter
)
{
- // Manual JSON to avoid pulling in System.Text.Json policy choices.
- // Output is a single object with metadata and an array of messages.
+ // Manual JSON to avoid System.Text.Json policy coupling.
w.Write("{\n \"exported_at\": \"");
w.Write(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
w.Write("\",\n \"plugin\": \"Hellion Chat\",\n");
@@ -194,7 +190,7 @@ internal static class MessageExporter
FilterDescription filter
)
{
- // Header line always written so empty exports are still importable.
+ // Header always written so empty exports remain importable.
w.WriteLine("Date,ChatType,ChatTypeName,Sender,Content,Receiver,ContentId");
var count = 0;
foreach (var m in messages)
diff --git a/HellionChat/GameFunctions/ChatBox.cs b/HellionChat/GameFunctions/ChatBox.cs
index 011867d..0fc9d13 100644
--- a/HellionChat/GameFunctions/ChatBox.cs
+++ b/HellionChat/GameFunctions/ChatBox.cs
@@ -15,17 +15,10 @@ public unsafe class ChatBox
mes->Dtor(true);
}
- public static void SendMessage(string message)
- {
- var bytes = ValidateMessage(message);
- SendMessageUnsafe(bytes);
- }
+ public static void SendMessage(string message) => SendMessageUnsafe(ValidateMessage(message));
- // Validation split out so the deterministic checks (UTF-8 length, sanitise
- // round-trip) can run in xUnit without ClientStructs game memory. The
- // sanitiser is injectable so tests can pin throw behaviour without invoking
- // Utf8String->SanitizeString, which only resolves in-process. Returns the
- // already-encoded bytes so SendMessage doesn't pay GetBytes twice.
+ // sanitiserOverride allows xUnit to bypass Utf8String->SanitizeString (game memory only).
+ // Returns encoded bytes so SendMessage avoids a second GetBytes call.
// TEST-MIRROR: ../../../Hellion Build test/GameFunctions/ChatBoxTests.cs
internal static byte[] ValidateMessage(
string message,
@@ -49,11 +42,9 @@ public unsafe class ChatBox
private static string SanitiseText(string text)
{
var uText = Utf8String.FromString(text);
-
uText->SanitizeString((AllowedEntities)0x27F);
var sanitised = uText->ToString();
uText->Dtor(true);
-
return sanitised;
}
}
diff --git a/HellionChat/GameFunctions/GameFunctions.cs b/HellionChat/GameFunctions/GameFunctions.cs
index 80ecbb1..770e6f6 100755
--- a/HellionChat/GameFunctions/GameFunctions.cs
+++ b/HellionChat/GameFunctions/GameFunctions.cs
@@ -47,7 +47,6 @@ internal unsafe class GameFunctions : IDisposable
Chat = new Chat(Plugin);
Plugin.GameInteropProvider.InitializeFromAttributes(this);
-
ResolveTextCommandPlaceholderHook?.Enable();
}
@@ -55,36 +54,24 @@ internal unsafe class GameFunctions : IDisposable
{
Chat.Dispose();
KeybindManager.Dispose();
-
ResolveTextCommandPlaceholderHook?.Dispose();
-
Marshal.FreeHGlobal(PlaceholderNamePtr);
}
- internal void SendFriendRequest(string name, ushort world)
- {
+ internal void SendFriendRequest(string name, ushort world) =>
ListCommand(name, world, "friendlist");
- }
- internal void AddToBlacklist(string name, ushort world)
- {
- ListCommand(name, world, "blist");
- }
+ internal void AddToBlacklist(string name, ushort world) => ListCommand(name, world, "blist");
- internal void AddToMuteList(ulong accountId, ulong contentId, string name, short worldId)
- {
+ internal void AddToMuteList(ulong accountId, ulong contentId, string name, short worldId) =>
AgentMutelist.Instance()->Add(accountId, contentId, name, worldId);
- }
- internal void AddToTermsList(SeString content)
- {
+ internal void AddToTermsList(SeString content) =>
AgentTermFilter.Instance()->OpenNewFilterWindow(content.EncodeWithNullTerminator());
- }
private void ListCommand(string name, ushort world, string commandName)
{
var worldRow = Sheets.WorldSheet.GetRow(world);
-
ReplacementName = $"{name}@{worldRow.Name.ToString()}";
ChatBox.SendMessage($"/{commandName} add {Placeholder}");
}
@@ -108,7 +95,6 @@ internal unsafe class GameFunctions : IDisposable
{
for (var i = 0; i < 4; i++)
SetAddonInteractable($"ChatLogPanel_{i}", interactable);
-
SetAddonInteractable("ChatLog", interactable);
}
@@ -124,7 +110,6 @@ internal unsafe class GameFunctions : IDisposable
var agent = AgentItemDetail.Instance();
var addon = GetAddon("ItemDetail");
- // atkStage ain't gonna be null or we have bigger problems
if (agent == null || addon == null)
return;
@@ -133,23 +118,19 @@ internal unsafe class GameFunctions : IDisposable
agent->Index = 0;
agent->Flag1 &= 0xEF;
agent->ItemId = id;
- // agent->Flag2 = 1;
- // agent->Flag3 = 0;
- // TODO: Revert whenever CS is merged
+
+ // TODO: Revert when CS offset lands in a release build.
*(byte*)((nint)agent + 0x21A) = 1;
*(byte*)((nint)agent + 0x21E) = 0;
- // This just probably needs to be set
agent->AddonId = addon->Id;
-
- // Skips early return
atkStage->TooltipManager.TooltipType |= 2;
addon->Show(false, 15);
}
internal static void CloseItemTooltip()
{
- // hide addon first to prevent the "addon close" sound
+ // Hide addon first to suppress the "addon close" sound.
var addon = GetAddon("ItemDetail");
if (addon != null)
addon->Hide(true, false, 0);
@@ -167,7 +148,7 @@ internal unsafe class GameFunctions : IDisposable
internal static void OpenPartyFinder()
{
- // this whole method: 6.05: 84433A (FF 97 ?? ?? ?? ?? 41 B4 01)
+ // 6.05: 84433A (FF 97 ?? ?? ?? ?? 41 B4 01)
var lfg = AgentLookingForGroup.Instance();
if (lfg->IsAgentActive())
{
@@ -188,15 +169,10 @@ internal unsafe class GameFunctions : IDisposable
}
}
- internal static bool IsMentor()
- {
- return PlayerState.Instance()->IsMentor();
- }
+ internal static bool IsMentor() => PlayerState.Instance()->IsMentor();
- internal static InfoProxyCommonList.CharacterData[] GetFriends()
- {
- return InfoProxyFriendList.Instance()->CharDataSpan.ToArray();
- }
+ internal static InfoProxyCommonList.CharacterData[] GetFriends() =>
+ InfoProxyFriendList.Instance()->CharDataSpan.ToArray();
internal static void OpenQuestLog(RowRef quest)
{
@@ -223,20 +199,12 @@ internal unsafe class GameFunctions : IDisposable
AgentQuestJournal.Instance()->OpenForQuest(questId, 1);
}
- internal static void OpenPartyFinder(uint id)
- {
+ internal static void OpenPartyFinder(uint id) =>
AgentLookingForGroup.Instance()->OpenListing(id);
- }
- internal static void OpenAchievement(uint id)
- {
- AgentAchievement.Instance()->OpenById(id);
- }
+ internal static void OpenAchievement(uint id) => AgentAchievement.Instance()->OpenById(id);
- internal static bool IsInInstance()
- {
- return Plugin.Condition[ConditionFlag.BoundByDuty56];
- }
+ internal static bool IsInInstance() => Plugin.Condition[ConditionFlag.BoundByDuty56];
internal static bool TryOpenAdventurerPlate(ulong playerId)
{
@@ -255,8 +223,7 @@ internal unsafe class GameFunctions : IDisposable
internal static void ClickNoviceNetworkButton()
{
var agent = AgentChatLog.Instance();
- // case 3
- var value = new AtkValue { Type = ValueType.Int, Int = 3 };
+ var value = new AtkValue { Type = ValueType.Int, Int = 3 }; // case 3
var result = 0;
var vf0 = *(delegate* unmanaged*)
agent->VirtualTable;
@@ -275,9 +242,8 @@ internal unsafe class GameFunctions : IDisposable
byte a4
)
{
- // The detour is only invoked through the hook, so the hook should
- // never be null here, but the nullable field declaration forces us
- // to handle the theoretical race during teardown.
+ // Hook field is nullable due to the Signature attribute, but will never
+ // be null during normal execution; guard covers the teardown race only.
if (ResolveTextCommandPlaceholderHook is null)
return nint.Zero;
@@ -285,9 +251,7 @@ internal unsafe class GameFunctions : IDisposable
if (ReplacementName == null || placeholder != Placeholder)
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
- // The fixed buffer is 128 bytes; UTF-8 + null-terminator must fit.
- // FFXIV player names plus an @World suffix should never approach this
- // limit, but a malformed ReplacementName must not overflow the buffer.
+ // Guard against a malformed ReplacementName overflowing the 128-byte buffer.
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
if (byteCount >= PlaceholderBufferSize)
{
@@ -300,7 +264,6 @@ internal unsafe class GameFunctions : IDisposable
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
ReplacementName = null;
-
return PlaceholderNamePtr;
}
}
diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml
index 2e41e34..13034d1 100755
--- a/HellionChat/HellionChat.yaml
+++ b/HellionChat/HellionChat.yaml
@@ -1,57 +1,26 @@
name: Hellion Chat
-author: JonKazama-Hellion
-punchline: Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)
+author: Jon Kazama (Hellion Forge)
+punchline: A Hellion Forge plugin — privacy-focused chat replacement for FFXIV, built for EU, US and JP data rules.
description: |-
- Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV
- based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally
- removed (the optional webinterface) and a stack of privacy controls is
- added on top. Tabs, channel filters, RGB colours, emotes, screenshot
- mode, IPC integration and the chat replacement window itself work the
- same. The webinterface is intentionally not part of Hellion Chat because
- it serves a different use case from the smaller default footprint this
- plugin is built around.
+ Chat replacement for FINAL FANTASY XIV with privacy controls built around
+ EU, US and JP data-protection rules.
- On top of that, Hellion Chat adds privacy and data-handling controls
- designed to align with the modern data protection rules that apply
- across the EU, the United States and Japan. By default only your own
- conversations are stored; messages from strangers, NPCs and system
- spam stay out of the database. Retention windows are configurable per
- channel, history can be wiped retroactively, and stored data can be
- exported on demand.
-
- Key privacy and data-handling features:
+ By default only your own conversations are stored. Public chat, NPC
+ dialogue and system messages stay out of the database unless you opt in.
+ Retention windows are configurable per channel, history can be wiped
+ retroactively, and everything can be exported on demand.
+ Features:
- Channel whitelist with a Privacy-First default
- Per-channel retention with a daily background sweep
- - Retroactive cleanup with a Ctrl+Shift confirm
+ - Retroactive cleanup (Ctrl+Shift confirm)
- Export to Markdown, JSON or CSV
- - First-run wizard with three preset profiles (Privacy-First, Casual,
- Full History)
- - Bilingual UI (English and German) with live language switching
- - Independent plugin state — own config file and database directory,
- so Hellion Chat does not share state with upstream Chat 2
+ - First-run wizard with three preset profiles
+ - Bilingual UI (EN/DE) with live language switching
+ - Own config and database — no shared state with other plugins
- 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
- tint and icon get a per-tab cache, and the status bar gates
- its tab aggregation behind the same one-second cache it uses
- for the format strings.
-
- v1.4.3 — Plugin-Load Async-Init plus Repo-Cutover. Plugin
- migrated to Dalamud's IAsyncDalamudPlugin so the heavy work
- (migrations, service allocations, window construction, hook
- subscription) runs in LoadAsync without blocking Dalamud's
- UI. Schema-gate replaces the v9 → v16 migration chain;
- configs on schema v16+ load directly. Custom-repo URL moves
- to gitea.hellion-forge.cloud, the GitHub repo stays as a
- frozen v1.4.2 snapshot.
-
- Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
-
- Modding & support: join the Hellion Forge Discord at
- https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
- other Hellion Online Media plugins/tools.
+ Based on Chat 2 by Infi and Anna (EUPL-1.2).
+ Support: https://discord.gg/X9V7Kcv5gR
repo_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat
accepts_feedback: true
icon_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png
@@ -66,104 +35,30 @@ tags:
- Replacement
- Privacy
changelog: |-
- **Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover (2026-05-08)**
+ **v1.4.3 — Faster plugin load + new repo (2026-05-08)**
- Plugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin`
- API. The constructor now does only the bootstrap-essentials
- (config load, language init, conflict detection); migrations,
- service allocations, window construction and hook subscription
- move to LoadAsync. Dalamud can keep its UI responsive while the
- heavy work runs.
+ Heavy startup work (migrations, hooks, windows) now runs async so
+ Dalamud's UI stays responsive during load. Load time is comparable
+ to v1.4.2 — this is the foundation for v1.4.4 optimisations.
- - IAsyncDalamudPlugin two-phase load with per-line CaptureFailure
- in DisposeAsync (mirrors LightlessSync's pattern); idempotency
- guard protects against reload races
- - Schema-gate replaces the v9 → v16 migration chain. Configs
- on schema v16+ load directly; older configs trigger an
- "install v1.4.2 first" error so the historic migration
- path stays intact
- - AutoTranslate.PreloadCache moved off the load path. First
- use may have a sub-second hitch instead of every-load; the
- upstream chose differently, we accept first-use latency
- - FontManager.BuildFonts is called sync at the start of
- LoadAsync; Dalamud rebuilds the font atlas on its own
- pipeline so the custom Hellion-Exo2 font appears with a
- brief font-pop after load (matches ChatTwo's behaviour)
- - Custom-repo URL moved to gitea.hellion-forge.cloud/
- JonKazama-Hellion/HellionChat. GitHub repo stays as a
- frozen v1.4.2 snapshot; new releases ship from Gitea.
- Existing testers need to update the custom-repo URL once
- - Plugin-load time in this release sits at ~3.7 s median
- (5 reloads), comparable to v1.4.2. Async migration is
- foundational for v1.4.4 Lazy-Init optimisations rather
- than an immediate user-perceived win
-
- Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
-
- Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
-
- **Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**
-
- Third sub-patch of the v1.4.x Polish Sweep series. Per-frame
- allocations from the chat-log render path eliminated.
-
- - DrawMessages card-mode hoists theme/drawList/winLeft/winRight/
- borderColorAbgr out of the per-message loop. About 500
- redundant calls per frame at 100 visible messages, multiplied
- by every pop-out window
- - Auto-tell tab tint and icon use a per-tab cache. Hash
- computation and string allocation only happen when the tell
- target name or world drifts. AutoTellTabTint stays a pure
- hash helper; cache lives in a thin TabTintCache wrapper
- - Status bar gates its tab aggregation behind the same
- one-second cache it already used for the format strings.
- LINQ Sum and Count replaced with a single foreach pass
- that runs on roughly 1% of frames
-
- Realistic frame-time recovery: 2-5% in typical scenes, more
- on pop-out-heavy setups because the card-border hoist scales
- per window.
-
- Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
-
- Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
-
- **Hellion Chat 1.4.1 — Theme Engine Performance**
-
- Second sub-patch of the v1.4.x Polish Sweep series. Heap
- pressure from the theme engine's per-frame render path
- removed, plus a tenth built-in theme and hardening for
- the custom-theme hot-reload.
-
- - Theme records carry a pre-computed ABGR-packed cache
- for every color slot; cache is filled when the theme
- is registered and refreshed defensively on every
- Switch()
- - HellionStyle.PushGlobal reads ABGR values from the
- cache instead of calling ColourUtil.RgbaToAbgr per
- slot per frame; ~13 % render-time recovery measured
- in typical scenes (plan estimate was 2–6 %, real
- ~10–15 %)
- - ThemeRegistry custom-theme reload distinguishes a
- recoverable file lock (editor mid-save) from a
- permanent IO failure; locked themes keep their
- last-known-good snapshot and retry on the next
- lookup instead of dropping out of the picker
- - New built-in: Synthwave Sunset — Hot Magenta + Cyan
- on midnight violet, 80s neon-grid vibes; tenth theme
- in the picker
- - Author credits refreshed: brand themes are credited
- as "Hellion Forge"; Mint Grove and Forge Merchantman
- now credited to Carla Beleandis as a community thanks
-
- No schema bump, no user-visible behaviour change other
- than smoother frames on GC-sensitive setups and one
- additional colour option.
-
- Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
-
- Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
+ - Two-phase async load via IAsyncDalamudPlugin
+ - Schema-gate replaces the v9→v16 migration chain; old configs
+ require a v1.4.2 install first
+ - AutoTranslate cache loads on first use instead of every startup
+ - Custom font (Hellion-Exo2) appears with a brief pop after load
+ - Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL
---
- Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
+ **v1.4.2 — Smoother frames in the chat log**
+
+ Per-frame allocations in the chat-log render path eliminated.
+ 2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.
+
+ - Card-mode: theme/border invariants hoisted out of the per-message loop
+ - Auto-tell tab tint and icon cached per tab
+ - Status bar aggregation runs on ~1% of frames instead of every frame
+
+ ---
+
+ Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
diff --git a/HellionChat/Ipc/ExtraChat.cs b/HellionChat/Ipc/ExtraChat.cs
index f77e367..4258d54 100644
--- a/HellionChat/Ipc/ExtraChat.cs
+++ b/HellionChat/Ipc/ExtraChat.cs
@@ -26,10 +26,9 @@ public sealed class ExtraChat : IDisposable
internal (string, uint)? ChannelOverride { get; set; }
- // Volatile reference: IPC callbacks (OnChannelCommandColours/OnChannelNames) fire on a
- // Dalamud-dispatcher thread while the ImGui thread reads the IReadOnlyDictionary projections.
- // Reference assignment is atomic on x64, but the JIT (especially Mono on Wine/Linux) needs
- // the volatile barrier to guarantee visibility across threads. See AUDIT-2026-05-05 [SEC-01].
+ // volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these.
+ // Reference assignment is atomic on x64, but the barrier ensures visibility
+ // across threads (especially Mono/Wine). See AUDIT-2026-05-05 [SEC-01].
private volatile Dictionary ChannelCommandColoursInternal = new();
internal IReadOnlyDictionary ChannelCommandColours =>
ChannelCommandColoursInternal;
@@ -54,6 +53,7 @@ public sealed class ExtraChat : IDisposable
OverrideChannelGate.Subscribe(OnOverrideChannel);
ChannelCommandColoursGate.Subscribe(OnChannelCommandColours);
ChannelNamesGate.Subscribe(OnChannelNames);
+
try
{
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
@@ -61,7 +61,7 @@ public sealed class ExtraChat : IDisposable
}
catch (Exception ex)
{
- // ExtraChat is optional; missing IPC peer is normal when the plugin isn't loaded.
+ // ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
}
}
@@ -75,22 +75,11 @@ public sealed class ExtraChat : IDisposable
private void OnOverrideChannel(OverrideInfo info)
{
- if (info.Channel == null)
- {
- ChannelOverride = null;
- return;
- }
-
- ChannelOverride = (info.Channel, info.Rgba);
+ ChannelOverride = info.Channel == null ? null : (info.Channel, info.Rgba);
}
- private void OnChannelCommandColours(Dictionary obj)
- {
+ private void OnChannelCommandColours(Dictionary obj) =>
ChannelCommandColoursInternal = obj;
- }
- private void OnChannelNames(Dictionary obj)
- {
- ChannelNamesInternal = obj;
- }
+ private void OnChannelNames(Dictionary obj) => ChannelNamesInternal = obj;
}
diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs
index 4e8ca79..ae4f213 100644
--- a/HellionChat/MessageStore.cs
+++ b/HellionChat/MessageStore.cs
@@ -127,7 +127,6 @@ internal class MessageStore : IDisposable
private const int MessageQueryLimit = 10_000;
private string DbPath { get; }
-
private SqliteConnection Connection { get; set; }
internal static readonly MessagePackSerializerOptions MsgPackOptions =
@@ -147,10 +146,8 @@ internal class MessageStore : IDisposable
public void Dispose()
{
- // Pooling=false (set in Connect) avoids ClearAllPools, which is
- // provider-wide and would touch other plugins' SQLite connections.
- // GC.Collect was here as a defensive flush; removed because explicit
- // Close already releases everything we hold.
+ // Pooling=false avoids ClearAllPools which is provider-wide and
+ // would touch other plugins' SQLite connections.
Connection.Close();
Connection.Dispose();
}
@@ -176,7 +173,6 @@ internal class MessageStore : IDisposable
private void Migrate()
{
- // Get current user_version.
using var cmd = Connection.CreateCommand();
cmd.CommandText = "PRAGMA user_version;";
var userVersion = Convert.ToInt32(cmd.ExecuteScalar());
@@ -186,9 +182,7 @@ internal class MessageStore : IDisposable
{
case <= 0:
migrationsToDo.Add(Migrate0);
-
- // Migration support was only added in version 1. Migrate 0 is
- // idempotent.
+ // Migration support was only added in version 1. Migrate0 is idempotent.
migrationsToDo.Add(Migrate1);
migrationsToDo.Add(Migrate2);
migrationsToDo.Add(Migrate3);
@@ -238,7 +232,6 @@ internal class MessageStore : IDisposable
Plugin.Log.Information("Running migration 1: Adding Deleted column");
Connection.Execute(
@"
- -- Migration 1: Add Deleted column
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
"
);
@@ -251,7 +244,6 @@ internal class MessageStore : IDisposable
Plugin.Log.Information("Running migration 2: Adding Channel generated column");
Connection.Execute(
@"
- -- Migration 2: Add Channel generated column
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages (Channel);
"
@@ -262,9 +254,8 @@ internal class MessageStore : IDisposable
private bool ColumnExists(string table, string column)
{
- // PRAGMA does not accept SQLite parameter bindings. The table name is
- // a compile-time constant fed in from internal call sites, so the
- // interpolation cannot be reached from any user-controlled path.
+ // PRAGMA does not accept SQLite parameter bindings. Table name is a
+ // compile-time constant from internal call sites only.
using var cmd = Connection.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({table});";
using var reader = cmd.ExecuteReader();
@@ -280,9 +271,8 @@ internal class MessageStore : IDisposable
{
Plugin.Log.Information("Running migration 3: Fix log kinds to fit the new format");
- // Recovery for partially-applied Migrate3: if the schema is already
- // in its target shape (new columns exist, old Code column gone) but
- // user_version was never bumped, just record the version and exit.
+ // Recovery for partially-applied Migrate3: schema already in target
+ // shape but user_version was never bumped -- just record and exit.
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
{
Plugin.Log.Information(
@@ -294,15 +284,6 @@ internal class MessageStore : IDisposable
Connection.Execute(
@"
- -- Migration 3: Fix log kinds to fit the new format
- -- Add new ChatType, SourceKind, TargetKind (byte), SortCodeV2
- -- Migrate OldChatColumn
- -- ChatType = OldChatColumn & 0x7f
- -- SourceKind = log2(1 << ((OldChatColumn >> 11) & 0xF))
- -- TargetKind = trunc(log2(1 << ((OldChatColumn >> 7) & 0xF)))
- -- Virtual SortCodeV2 = ChatType << 16 | SourceKind << 8 | TargetKind
- -- Delete OldChatColumn, Virtual Channel
-
ALTER TABLE messages ADD COLUMN ChatType INTEGER;
CREATE INDEX IF NOT EXISTS idx_messages_chat_type ON messages (ChatType);
ALTER TABLE messages ADD COLUMN SourceKind INTEGER;
@@ -328,10 +309,8 @@ internal class MessageStore : IDisposable
{
Plugin.Log.Information($"Setting version {version}");
using var cmd = Connection.CreateCommand();
- // PRAGMA does not accept SQLite parameter bindings, and there is no
- // pragma_ function variant that can set the version either. The
- // version is a compile-time int from the migration sequence, never
- // user input.
+ // PRAGMA does not accept SQLite parameter bindings; version is a
+ // compile-time int from the migration sequence, never user input.
cmd.CommandText = $"PRAGMA user_version = {version};";
cmd.ExecuteNonQuery();
}
@@ -342,11 +321,8 @@ internal class MessageStore : IDisposable
PerformMaintenance();
}
- ///
- /// Returns a (ChatType, count) snapshot over non-deleted messages.
- /// Used by the Privacy tab to preview the impact of a retroactive
- /// cleanup before the user confirms.
- ///
+ // Returns a (ChatType, count) snapshot over non-deleted messages.
+ // Used by the Privacy tab to preview retroactive cleanup impact.
internal Dictionary GetMessageCountsByChatType()
{
var result = new Dictionary();
@@ -364,12 +340,9 @@ internal class MessageStore : IDisposable
return result;
}
- ///
- /// Deletes messages older than the per-channel retention window, with a
- /// global default for channels not listed explicitly. Cutoffs are
- /// computed from "now" at call time. Runs VACUUM only if anything was
- /// removed. Returns the number of rows deleted.
- ///
+ // Deletes messages older than the per-channel retention window, with a global
+ // default for unmapped channels. Runs VACUUM only if rows were removed.
+ // Returns the number of rows deleted.
internal long DeleteByRetentionPolicy(
IReadOnlyDictionary chatTypeDaysMap,
int defaultDays
@@ -408,10 +381,7 @@ internal class MessageStore : IDisposable
index++;
}
- // Catch-all for channels without an explicit override. "0" is
- // treated as "do not delete by default" — without an explicit
- // user override, unmapped channels stay forever instead of
- // getting wiped immediately.
+ // defaultDays=0 means "keep forever" for unmapped channels.
if (defaultDays > 0)
{
var defaultCutoff = nowMs - defaultDays * 86400000L;
@@ -439,21 +409,14 @@ internal class MessageStore : IDisposable
return deleted;
}
- ///
- /// Hard-deletes every message whose ChatType is not in the supplied
- /// allowlist, then VACUUMs the database to reclaim disk space.
- /// Returns the number of rows deleted.
- ///
+ // Hard-deletes every message whose ChatType is not in the allowlist,
+ // then VACUUMs. Returns the number of rows deleted.
internal long CleanupRetainOnly(IReadOnlyCollection allowedTypes)
{
if (allowedTypes.Count == 0)
- {
- // Defensive: refuse a "delete everything" disguised as a filter.
- // Use ClearMessages() if a full wipe is actually intended.
throw new InvalidOperationException(
"CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe."
);
- }
long deleted;
using (var cmd = Connection.CreateCommand())
@@ -493,14 +456,9 @@ internal class MessageStore : IDisposable
internal void UpsertMessage(Message message)
{
- // Hellion Chat privacy filter — drop disallowed ChatTypes before
- // they reach the storage layer (single source of truth, also
- // covers any future write paths e.g. webinterface backfill).
+ // Privacy filter -- drop disallowed ChatTypes before they reach storage.
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
{
- // Verbose-only: this fires for every dropped message, which is
- // the common case for users with a tight privacy whitelist. Keep
- // it for diagnostics but stay out of the default xllog stream.
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
return;
}
@@ -509,33 +467,11 @@ internal class MessageStore : IDisposable
cmd.CommandText =
@"
INSERT INTO messages (
- Id,
- Receiver,
- ContentId,
- Date,
- ChatType,
- SourceKind,
- TargetKind,
- Sender,
- Content,
- SenderSource,
- ContentSource,
- ExtraChatChannel,
- Deleted
+ Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
+ Sender, Content, SenderSource, ContentSource, ExtraChatChannel, Deleted
) VALUES (
- $Id,
- $Receiver,
- $ContentId,
- $Date,
- $ChatType,
- $SourceKind,
- $TargetKind,
- $Sender,
- $Content,
- $SenderSource,
- $ContentSource,
- $ExtraChatChannel,
- false
+ $Id, $Receiver, $ContentId, $Date, $ChatType, $SourceKind, $TargetKind,
+ $Sender, $Content, $SenderSource, $ContentSource, $ExtraChatChannel, false
)
ON CONFLICT (id) DO UPDATE SET
Receiver = excluded.Receiver,
@@ -580,13 +516,9 @@ internal class MessageStore : IDisposable
cmd.ExecuteNonQuery();
}
- ///
- /// Streams messages for export. Optional filters:
- /// - : limit to these ChatTypes
- /// - / : inclusive date range
- /// Result is sorted ascending by Date and excludes soft-deleted rows.
- /// Caller is responsible for disposing the enumerator.
- ///
+ // Streams messages for export, sorted ascending by Date, excluding soft-deleted rows.
+ // Optional filters: chatTypes, from/to inclusive date range.
+ // Caller is responsible for disposing the enumerator.
internal MessageEnumerator StreamForExport(
IReadOnlyCollection? chatTypes,
DateTimeOffset? from,
@@ -606,18 +538,8 @@ internal class MessageStore : IDisposable
cmd.CommandText =
@"
SELECT
- Id,
- Receiver,
- ContentId,
- Date,
- ChatType,
- SourceKind,
- TargetKind,
- Sender,
- Content,
- SenderSource,
- ContentSource,
- ExtraChatChannel
+ Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
+ Sender, Content, SenderSource, ContentSource, ExtraChatChannel
FROM messages
WHERE "
+ string.Join(" AND ", clauses)
@@ -633,12 +555,10 @@ internal class MessageStore : IDisposable
return new MessageEnumerator(cmd.ExecuteReader());
}
- ///
- /// Get the most recent messages.
- ///
- /// The receiver content ID to filter by. If null, no filtering is performed.
- /// Only show messages since this date. If null, no filtering is performed.
- /// The amount to return. Defaults to 10,000.
+ // Returns the most recent messages, oldest-first.
+ // receiver: filter by receiver ContentId (null = no filter)
+ // since: only include messages after this date (null = no filter)
+ // count: max rows to return, defaults to 10,000
internal MessageEnumerator GetMostRecentMessages(
ulong? receiver = null,
DateTimeOffset? since = null,
@@ -654,25 +574,14 @@ internal class MessageStore : IDisposable
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
var cmd = Connection.CreateCommand();
- // Select last N messages by date DESC, but reverse the order to get
- // them in ascending order.
+ // Select last N by date DESC, then reverse to ascending order.
cmd.CommandText =
@"
SELECT *
FROM (
SELECT
- Id,
- Receiver,
- ContentId,
- Date,
- ChatType,
- SourceKind,
- TargetKind,
- Sender,
- Content,
- SenderSource,
- ContentSource,
- ExtraChatChannel
+ Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
+ Sender, Content, SenderSource, ContentSource, ExtraChatChannel
FROM messages
"
+ whereClause
@@ -682,7 +591,7 @@ internal class MessageStore : IDisposable
)
ORDER BY Date ASC;
";
- cmd.CommandTimeout = 120; // this could take a while on slow computers
+ cmd.CommandTimeout = 120;
if (receiver != null)
cmd.Parameters.AddWithValue("$Receiver", receiver);
@@ -694,21 +603,10 @@ internal class MessageStore : IDisposable
return new MessageEnumerator(cmd.ExecuteReader());
}
- ///
- /// Hellion Chat — Auto-Tell-Tabs history preload.
- ///
- /// Returns up to tells exchanged with the named
- /// player, oldest-first, ready to be added to a freshly spawned auto
- /// tell tab. The Sender column is a serialized chunk blob, so SQL on its
- /// own cannot filter by player identity; we narrow with SQL on Receiver
- /// + ChatType (cheap, indexed) and let the client do the final
- /// PlayerPayload comparison on the result set.
- ///
- /// caps how many recent tells we scan
- /// before giving up. 500 covers around 10 days for an active greeter
- /// and stays well under the 20 ms budget required to keep the spawn on
- /// the message-processing worker thread.
- ///
+ // Returns up to limit tells exchanged with the named player, oldest-first.
+ // SQL narrows by Receiver + ChatType (indexed); client does the final
+ // PlayerPayload comparison. sqlScanLimit caps the scan to stay within
+ // the message-processing worker thread budget.
internal IReadOnlyList GetTellHistoryWithSender(
ulong receiver,
string senderName,
@@ -718,26 +616,14 @@ internal class MessageStore : IDisposable
)
{
if (limit <= 0)
- {
return [];
- }
using var cmd = Connection.CreateCommand();
cmd.CommandText =
@"
SELECT
- Id,
- Receiver,
- ContentId,
- Date,
- ChatType,
- SourceKind,
- TargetKind,
- Sender,
- Content,
- SenderSource,
- ContentSource,
- ExtraChatChannel
+ Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
+ Sender, Content, SenderSource, ContentSource, ExtraChatChannel
FROM messages
WHERE deleted = false
AND Receiver = $Receiver
@@ -756,27 +642,19 @@ internal class MessageStore : IDisposable
foreach (var message in enumerator)
{
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
- {
continue;
- }
collected.Add(message);
if (collected.Count >= limit)
- {
break;
- }
}
- // SQL was DESC (newest-first) so we hit the limit on the most
- // recent matching tells. Reverse to oldest-first for chronological
- // display in the tab.
+ // SQL was DESC (newest-first); reverse to oldest-first for tab display.
collected.Reverse();
return collected;
}
- ///
- /// Marks a message as deleted so it won't get returned in queries.
- ///
+ // Soft-deletes a message so it won't appear in queries.
internal void DeleteMessage(Guid id)
{
using var cmd = Connection.CreateCommand();
@@ -803,8 +681,6 @@ internal class MessageStore : IDisposable
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
- // Select last N messages by date DESC, but reverse the order to get
- // them in ascending order.
cmd.CommandText =
@"
SELECT COUNT(*)
@@ -816,7 +692,7 @@ internal class MessageStore : IDisposable
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
- cmd.CommandTimeout = 120; // this could take a while on slow computers
+ cmd.CommandTimeout = 120;
return (long)cmd.ExecuteScalar()!;
}
@@ -839,26 +715,14 @@ internal class MessageStore : IDisposable
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
- // Select last N messages by date DESC, but reverse the order to get
- // them in ascending order.
cmd.CommandText =
@"
SELECT
- Id,
- Receiver,
- ContentId,
- Date,
- ChatType,
- SourceKind,
- TargetKind,
- Sender,
- Content,
- SenderSource,
- ContentSource,
- ExtraChatChannel
+ Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
+ Sender, Content, SenderSource, ContentSource, ExtraChatChannel
FROM messages
" + whereClause;
- cmd.CommandTimeout = 120; // this could take a while on slow computers
+ cmd.CommandTimeout = 120;
if (receiver != null)
cmd.Parameters.AddWithValue("$Receiver", receiver);
@@ -888,23 +752,11 @@ internal class MessageStore : IDisposable
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
- // Select last N messages by date DESC, but reverse the order to get
- // them in ascending order.
cmd.CommandText =
@"
SELECT
- Id,
- Receiver,
- ContentId,
- Date,
- ChatType,
- SourceKind,
- TargetKind,
- Sender,
- Content,
- SenderSource,
- ContentSource,
- ExtraChatChannel
+ Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
+ Sender, Content, SenderSource, ContentSource, ExtraChatChannel
FROM messages
"
+ whereClause
@@ -912,7 +764,7 @@ internal class MessageStore : IDisposable
ORDER BY Date
LIMIT $Offset, $OffsetCount;
";
- cmd.CommandTimeout = 120; // this could take a while on slow computers
+ cmd.CommandTimeout = 120;
if (receiver != null)
cmd.Parameters.AddWithValue("$Receiver", receiver);
@@ -925,10 +777,8 @@ internal class MessageStore : IDisposable
return new MessageEnumerator(cmd.ExecuteReader());
}
- // Build "$prefix0,$prefix1,..." placeholder list and bind values to
- // the command. SQLite has no native array parameter, so we generate
- // the list at runtime and bind each entry under its own name. Used
- // for IN-clauses and similar dynamic-arity SQL fragments.
+ // Builds a "$prefix0,$prefix1,..." placeholder list and binds values to the command.
+ // SQLite has no native array parameter, so placeholders are generated per entry.
private static string BindIntList(SqliteCommand cmd, string prefix, IEnumerable values)
{
var names = new List();
@@ -951,8 +801,6 @@ internal class MessageEnumerator(DbDataReader reader)
{
private const int MaxErrorLogs = 10;
- // FailedIds and FailedCount are separate, because messages might fail to
- // even parse the ID field.
private readonly List FailedIds = [];
private int FailedCount;
public bool DidError => FailedCount > 0;
diff --git a/HellionChat/Privacy/PrivacyDefaults.cs b/HellionChat/Privacy/PrivacyDefaults.cs
index d7282b8..2a3e503 100644
--- a/HellionChat/Privacy/PrivacyDefaults.cs
+++ b/HellionChat/Privacy/PrivacyDefaults.cs
@@ -4,10 +4,9 @@ namespace HellionChat.Privacy;
internal static class PrivacyDefaults
{
- // Privacy-First default whitelist (DSGVO Art. 25 - Privacy by Default).
- // Only the player's own conversations are persisted out-of-the-box.
- // Public chat (Say/Shout/Yell), Novice Network, NPC dialogue, system
- // logs and battle messages are NOT persisted unless the user opts in.
+ // DSGVO Art. 25 (Privacy by Default): only the player's own conversations
+ // are persisted out-of-the-box. Public chat, NPC dialogue, system logs and
+ // battle messages require explicit opt-in.
internal static readonly IReadOnlySet PrivacyFirstWhitelist = new HashSet
{
ChatType.TellIncoming,
@@ -42,10 +41,8 @@ internal static class PrivacyDefaults
ChatType.ExtraChatLinkshell8,
};
- // Default retention windows per channel (in days). Channels not listed
- // here fall back to Configuration.RetentionDefaultDays. Reflects the
- // design spec: Tells 365, own-conversation channels 90, everything else
- // shorter via the global default.
+ // Per-channel retention in days. Unlisted channels fall back to
+ // Configuration.RetentionDefaultDays. Tells: 365, everything else: 90.
internal static readonly IReadOnlyDictionary DefaultRetentionDays =
new Dictionary
{
@@ -86,10 +83,9 @@ internal static class PrivacyDefaults
[ChatType.ExtraChatLinkshell8] = 90,
};
- // Casual profile = Privacy-First plus public chat (Say/Shout/Yell, both
- // emote types, Novice Network), kept for a short 24-hour window so the
- // last RP scene or shout trade is still searchable but third-party data
- // doesn't accumulate forever.
+ // Casual: Privacy-First + public chat (Say/Shout/Yell, emotes, Novice
+ // Network) with a 1-day window so recent RP/trade is searchable but
+ // third-party data doesn't accumulate.
internal static readonly IReadOnlySet CasualWhitelist = new HashSet(
PrivacyFirstWhitelist
)
diff --git a/HellionChat/Themes/Builtin/HellionSpectrum.cs b/HellionChat/Themes/Builtin/HellionSpectrum.cs
index 0554f42..697c9de 100644
--- a/HellionChat/Themes/Builtin/HellionSpectrum.cs
+++ b/HellionChat/Themes/Builtin/HellionSpectrum.cs
@@ -2,12 +2,7 @@ using HellionChat.Util;
namespace HellionChat.Themes.Builtin;
-// Hellion Spectrum: Deuteran/Protan-safe channel colours.
-// Palette derived from Bang Wong, "Points of view: Color blindness",
-// Nature Methods 8, 441 (2011). Channel identity (Tell pink, Yell yellow,
-// Shout orange, Party blue, FC green) is preserved per Channel-Identity-
-// Rule in docs/THEME-AUTHORING.md; tones are chosen so every channel
-// stays distinguishable under red-green colour-vision deficiency.
+// Deuteran/Protan-safe palette with preserved channel identity.
internal static class HellionSpectrum
{
public const string Slug = "hellion-spectrum";
@@ -57,9 +52,6 @@ internal static class HellionSpectrum
ChatColors: new ThemeChatColors(
new Dictionary
{
- // Hellion Spectrum — Wong/Okabe-Ito tones within FFXIV channel
- // identity. FC pulled slightly greener than vanilla cyan-teal so
- // Party-blue and FC-green stay separable under deuteran sim.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0E442"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#D55E00"),
diff --git a/HellionChat/Ui/AutoTellTabTint.cs b/HellionChat/Ui/AutoTellTabTint.cs
index 2ea733a..d6b26f2 100644
--- a/HellionChat/Ui/AutoTellTabTint.cs
+++ b/HellionChat/Ui/AutoTellTabTint.cs
@@ -1,34 +1,17 @@
namespace HellionChat.Ui;
-///
-/// Hash-Color-Tinting für Auto-Tell-Tabs in der Sidebar (v1.2.0).
-/// Differenziert Tells visuell ohne dass User pro Tab manuell ein
-/// Custom-Icon setzen muss. Gleicher Tell-Partner (Name+World) liefert
-/// konsistent dieselbe Farbe über Sessions hinweg.
-///
-/// Kuratierte 12-Farb-Palette aus dem Hellion-Theme-Pool: alle saturiert
-/// mid-bright, lesbar gegen Dark-Theme-Backgrounds. Bei realistischen
-/// 1-5 parallelen Tells ist Kollisions-Wahrscheinlichkeit gering.
-///
-/// Reine String-Logik (kein Dalamud-Dep) — testbar im HellionChat.Tests-
-/// Projekt das ohne Dalamud-Reference baut.
-///
+// Deterministic hash-based color and icon tinting for Auto-Tell sidebar tabs.
+// Same tell partner (name+world) always produces the same color and icon across
+// sessions. Pure string logic, no Dalamud dependency — testable without game refs.
internal static class AutoTellTabTint
{
- ///
- /// Fallback bei ungültigem Input (leerer Name, World=0). Standard-
- /// Text-Color (weiß) — passt mit existierendem TextPrimary-Default
- /// zusammen, sodass die Sidebar visuell konsistent bleibt.
- ///
+ // Fallback for invalid input (empty name or world=0). White matches
+ // TextPrimary default so the sidebar stays visually consistent.
public const uint Fallback = 0xFFFFFFFFu;
- ///
- /// 12 saturierte mid-bright Farben aus den 5 Built-In-Themes
- /// (Hellion-Arctic, Chat2-Klassik, Event-Horizon, Moonlit-Bloom,
- /// Mint-Grove). Reihenfolge ist deterministisch — Hash-Index wählt
- /// Farbe per Modulo. RGBA-Format (passt zu ColourUtil.RgbaToAbgr-
- /// Konvention im restlichen Code).
- ///
+ // 12 saturated mid-bright colors from the built-in theme pool, readable
+ // on dark backgrounds. Collision risk is low at realistic 1-5 active tells.
+ // RGBA format, matching ColourUtil.RgbaToAbgr convention.
public static readonly IReadOnlyList Palette = new uint[]
{
0x00BED2FFu, // Arctic Cyan
@@ -45,30 +28,19 @@ internal static class AutoTellTabTint
0xE85D04FFu, // Deep Ember
};
- ///
- /// Liefert eine konsistente Tint-Color für einen Tell-Partner.
- /// Hash basiert auf "Name@World" — Cross-World-Namen kollidieren
- /// nur bei Hash-Bucket-Kollision, nicht durch Identitäts-Annahme.
- ///
public static uint For(string name, uint world)
{
if (string.IsNullOrEmpty(name) || world == 0)
return Fallback;
- // GetHashCode kann negativ sein; Bitmaske auf positive Range
- // damit Modulo-Division immer einen validen Index liefert.
+ // Mask to positive range so modulo always yields a valid index.
var key = $"{name}@{world}";
var hash = (uint)(key.GetHashCode() & 0x7FFFFFFF);
return Palette[(int)(hash % Palette.Count)];
}
- ///
- /// Tell-spezifischer Icon-Pool. 7 visuell distinkte FontAwesome-Glyphen
- /// die im Tell-Kontext sinnvoll wirken (envelope = Tell-Default, star/
- /// heart/bell = personalisiert, bookmark/flag/fire = markiert/wichtig).
- /// Bewusst kein cog/comment/users — die wären für System-/Group-Tabs
- /// reserviert und würden im Tell-Bereich verwirrend wirken.
- ///
+ // 7 visually distinct FA glyphs that make sense in a tell context.
+ // Excludes cog/comment/users — those read as system or group tabs.
public static readonly IReadOnlyList IconPool = new[]
{
"envelope",
@@ -80,26 +52,17 @@ internal static class AutoTellTabTint
"fire",
};
- ///
- /// Fallback-Icon bei ungültigem Input. "envelope" passt semantisch zum
- /// Tell-Kontext besser als das alte hardcoded "clock".
- ///
+ // "envelope" matches the tell context better than the old hardcoded "clock".
public const string IconFallback = "envelope";
- ///
- /// Liefert ein konsistentes Icon-Glyph für einen Tell-Partner.
- /// Nutzt einen anderen Hash-Bias als For() (Color), damit Icon und
- /// Color unabhängig variieren — gibt 7 × 12 = 84 distinct Combinations.
- ///
public static string IconFor(string name, uint world)
{
if (string.IsNullOrEmpty(name) || world == 0)
return IconFallback;
- // Anderer Hash-Bias als For() (verschiedene Modulo-Basis): wir
- // nutzen "world@name" statt "name@world" damit Icon und Color
- // nicht synchron variieren. Ohne Bias-Trennung würden alle Tells
- // mit derselben Color auch dasselbe Icon haben.
+ // Reversed key ("world@name") gives icon and color independent variation
+ // so the same tell partner doesn't always get the same color+icon pair.
+ // 7 icons x 12 colors = 84 distinct combinations.
var key = $"{world}@{name}";
var hash = (uint)(key.GetHashCode() & 0x7FFFFFFF);
return IconPool[(int)(hash % IconPool.Count)];
diff --git a/HellionChat/Ui/ChatInputBar.cs b/HellionChat/Ui/ChatInputBar.cs
index bf801a3..511facb 100644
--- a/HellionChat/Ui/ChatInputBar.cs
+++ b/HellionChat/Ui/ChatInputBar.cs
@@ -8,16 +8,10 @@ using HellionChat.Util;
namespace HellionChat.Ui;
-// Hellion Chat — v0.6.0 input bar component for pop-out windows.
-//
-// Pragmatischer Refactor-Scope: Render() ist ein leerer Marker-Stub für
-// das Hauptfenster — der bestehende Input-Layer in ChatLogWindow bleibt
-// unangetastet, weil ein 400-Zeilen-Extract aus einem 1926-Zeilen-File
-// das v0.6.0-Risiko unverhältnismäßig erhöhen würde. Pop-Outs nutzen
-// ausschließlich RenderCompact(), das ist der ganze v0.6.0-Mehrwert.
-// Sollte das Hauptfenster selber später eine Compact-Variante brauchen
-// (oder das große Extract sich aus anderem Grund lohnen), kann Render()
-// in einem späteren Cycle gefüllt werden.
+// Input bar component for pop-out windows. Render() is a stub — the main
+// window input layer stays in ChatLogWindow to avoid a high-risk extract.
+// RenderCompact() is the only v0.6.0 deliverable; Render() can be filled
+// in a later cycle if needed.
public sealed class ChatInputBar
{
private readonly Plugin _plugin;
@@ -35,22 +29,17 @@ public sealed class ChatInputBar
public InputState State => _state;
public bool IsFocused { get; private set; }
- // Stub. v0.6.0 belässt den Hauptfenster-Input wie er ist.
+ // Stub — main window input is handled in ChatLogWindow.
public void Render() { }
- // Compact rendering for pop-out windows.
+ // Compact layout for pop-out windows: channel icon button left, text
+ // input right. Auto-translate is intentionally excluded — the upstream
+ // popup isn't instanciable per window without a larger refactor, and
+ // typical pop-out use cases rarely need it. Can be added later if
+ // tester feedback warrants it.
//
- // v0.6.0 Compact-Layout: Channel-Icon-Button links (Background-Farbe
- // aus ChatColours), Text-Input rechts daneben. Auto-Translate-Picker
- // ist bewusst NICHT im Compact-Mode (Spec-Abweichung Layout D → A).
- // Rechtfertigung: das Hauptfenster-Auto-Complete-Popup ist nicht ohne
- // grossen Refactor pro Window instanzierbar; typische Pop-Out-Use-Cases
- // (FC-Greeter, Club-Hostess) brauchen Auto-Translate selten dort.
- // Eigene Compact-Auto-Complete-Implementation kann ein späterer
- // Cycle nachreichen wenn Tester-Feedback das verlangt.
- //
- // Channel-Switch wirkt via Plugin.Functions.Chat global (FFXIV-API).
- // Pro Pop-Out unabhängig bleiben Text-Buffer und History-Cursor.
+ // Channel switching is global via Plugin.Functions.Chat (FFXIV API).
+ // Text buffer and history cursor are independent per pop-out.
public void RenderCompact()
{
var tab = _activeTabAccessor();
@@ -64,18 +53,15 @@ public sealed class ChatInputBar
private void DrawCompactInput(Tab tab)
{
- // Input takes the whole remaining width — no auto-translate button
- // reserved on the right side in v0.6.0 (see RenderCompact comment).
var inputWidth = ImGui.GetContentRegionAvail().X;
if (inputWidth < 60f)
inputWidth = 60f;
ImGui.SetNextItemWidth(inputWidth);
- // CallbackHistory wires up Up/Down navigation against the shared
- // InputHistoryService. Submit is detected the same way the main
- // window does it: via IsItemDeactivated + Enter, NOT EnterReturnsTrue
- // (matching v0.5.x ChatLogWindow.cs behavior).
+ // CallbackHistory wires Up/Down navigation to InputHistoryService.
+ // Submit detected via IsItemDeactivated + Enter, not EnterReturnsTrue
+ // (matches ChatLogWindow behavior).
const ImGuiInputTextFlags flags = ImGuiInputTextFlags.CallbackHistory;
ImGui.InputText(
$"##chat-compact-input-{tab.Identifier}",
@@ -100,9 +86,8 @@ public sealed class ChatInputBar
private void SubmitCompact(Tab tab) =>
CompactInputSubmitter.TrySubmit(_state, tab, _host.SendChatBoxFromExternal);
- // History-navigation callback for the compact input. Cursor math is
- // delegated to CompactInputHistoryNavigator; only the ImGui buffer
- // splice stays here because it needs the live callback data.
+ // History navigation callback. Cursor math delegated to
+ // CompactInputHistoryNavigator; ImGui buffer splice stays here.
// TEST-MIRROR: ../_Helpers/CompactInputHistoryNavigator.cs
private int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
{
@@ -148,7 +133,7 @@ public sealed class ChatInputBar
var v3 = ColourUtil.RgbaToVector3(rgba);
var bg = new Vector4(v3.X, v3.Y, v3.Z, 1f);
- // Compute readable foreground — black on bright, white on dark
+ // Black foreground on bright backgrounds, white on dark.
var luminance = 0.2126f * v3.X + 0.7152f * v3.Y + 0.0722f * v3.Z;
var fg = luminance > 0.55f ? new Vector4(0f, 0f, 0f, 1f) : new Vector4(1f, 1f, 1f, 1f);
@@ -160,8 +145,7 @@ public sealed class ChatInputBar
using (ImRaii.PushColor(ImGuiCol.ButtonActive, bg))
using (ImRaii.PushColor(ImGuiCol.Text, fg))
{
- // Single-letter glyph derived from the channel — quick visual cue
- // until we have a proper icon font available in the compact bar.
+ // Single-letter glyph as a quick visual cue until a proper icon font lands.
var label = ChannelGlyph(inputType);
if (
ImGui.Button($"{label}##chan-compact", new Vector2(buttonSize, buttonSize))
@@ -171,13 +155,9 @@ public sealed class ChatInputBar
}
if (tab.Channel is not null && ImGui.IsItemHovered())
- {
ImGui.SetTooltip(Resources.Language.ChatLog_SwitcherDisabled);
- }
else if (ImGui.IsItemHovered())
- {
ImGui.SetTooltip(inputType.Name());
- }
using (var popup = ImRaii.Popup(popupId))
{
@@ -221,17 +201,12 @@ public sealed class ChatInputBar
_ => "?",
};
- // Forwards a tab-cycle keybind delta to the host so all windows
- // navigate the same active-tab pointer (single source of truth).
- public void HandleKeybindForward(int delta)
- {
- _host.ChangeTabDelta(delta);
- }
+ // Forwards a tab-cycle keybind delta to the host (single source of truth).
+ public void HandleKeybindForward(int delta) => _host.ChangeTabDelta(delta);
}
-// Per-window input state. Each ChatInputBar instance owns one of these
-// so pop-outs and the main window keep independent buffers and channels
-// (State-Sync-Entscheidung A in the v0.6.0 spec).
+// Per-window input state. Each ChatInputBar owns one so pop-outs and the
+// main window keep independent buffers and history cursors.
public sealed class InputState
{
public string Buffer = string.Empty;
diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs
index dfb653d..e2b1e6e 100644
--- a/HellionChat/Ui/ChatLogWindow.cs
+++ b/HellionChat/Ui/ChatLogWindow.cs
@@ -52,10 +52,8 @@ public sealed class ChatLogWindow : Window
private int ActivatePos = -1;
internal string Chat = string.Empty;
- // Hellion Chat — v0.6.0 input history was extracted into
- // InputHistoryService so pop-out windows with their own ChatInputBar
- // share the same Up/Down history with the main window. The cursor
- // stays window-local because each window navigates independently.
+ // Input history extracted into InputHistoryService so pop-out windows share
+ // the same Up/Down history. Cursor stays window-local (independent navigation).
private int InputBacklogIdx = -1;
public bool TellSpecial;
private readonly Stopwatch LastResize = new();
@@ -74,11 +72,8 @@ public sealed class ChatLogWindow : Window
public Vector2 LastWindowPos { get; private set; } = Vector2.Zero;
public Vector2 LastWindowSize { get; private set; } = Vector2.Zero;
- // Window position recovery: guards against off-screen positions after a
- // display layout change (monitor disconnected, resolution changed). On
- // the first draw after plugin load we run a one-shot bounds check to see
- // whether the stored position still overlaps any visible viewport area.
- // The manual reset button in the settings forces the position regardless.
+ // Guards against off-screen positions after a display layout change.
+ // One-shot bounds check on first draw; manual reset button bypasses it.
private bool DidOnLoadBoundsCheck;
internal bool RequestPositionReset { get; set; }
@@ -112,9 +107,7 @@ public sealed class ChatLogWindow : Window
IsOpen = true;
RespectCloseHotkey = false;
DisableWindowSounds = true;
- // AllowBackgroundBlur wird nach AddWindow zentral in Plugin.Setup
- // für alle registrierten Windows gesetzt — keine Per-Window-Logik
- // hier nötig.
+ // AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow.
PayloadHandler = new PayloadHandler(this);
HandlerLender = new Lender(() => new PayloadHandler(this));
@@ -122,10 +115,8 @@ public sealed class ChatLogWindow : Window
SetUpTextCommandChannels();
SetUpAllCommands();
- // Cache the registered wrapper instances so Dispose can detach the same
- // event objects the constructor attached to, without going through
- // Register() again (which would re-create the wrapper if the command
- // happened to be missing from the dictionary).
+ // Cache wrapper instances so Dispose can detach the same event objects
+ // without going through Register() again.
_clearHellionCommand = Plugin.Commands.Register(
"/clearhellion",
"Clear the Hellion Chat log"
@@ -397,11 +388,10 @@ public sealed class ChatLogWindow : Window
}
}
+ // Delegates to InputHistoryService so pop-out ChatInputBar instances share
+ // history. Deduplication lives inside the service.
private void AddBacklog(string message)
{
- // v0.6.0 — delegates to the shared InputHistoryService so pop-out
- // ChatInputBar instances see the same history. Move-to-newest
- // deduplication lives inside the service.
InputHistoryService.Push(message);
}
@@ -417,15 +407,12 @@ public sealed class ChatLogWindow : Window
if (Plugin.Config.PreviewPosition is PreviewPosition.Inside)
height -= Plugin.InputPreview.PreviewHeight;
- // Hellion Chat v0.6.1 — Header-Toolbar rendert auf Window-Ebene über
- // einem horizontalen Layout-Pfad und wird von GetContentRegionAvail
- // hier drin NICHT automatisch berücksichtigt, daher expliziter Abzug.
- // Banner dagegen rendert in DrawChatLog VOR diesem ganzen Block und
- // ImGui zieht seine Höhe automatisch von GetContentRegionAvail ab,
- // weil der Cursor schon weiter unten steht — kein eigener Abzug.
+ // Header toolbar height is not subtracted by GetContentRegionAvail automatically
+ // (it renders outside the normal layout path), so we subtract it explicitly.
+ // The hint banner renders before this block so ImGui already accounts for it.
height -= ImGui.GetFrameHeightWithSpacing();
- // v1.2.0 — Status-Bar am Window-Boden reserviert 22 px + 2 px Spacing.
+ // Status bar at the window bottom reserves 22px + 2px spacing.
height -= StatusBar.Height + 2;
return height;
@@ -659,10 +646,8 @@ public sealed class ChatLogWindow : Window
LastWindowSize = currentSize;
LastWindowPos = ImGui.GetWindowPos();
- // Window position recovery. Manual reset takes precedence and snaps
- // the window to the safe default unconditionally; the one-shot
- // on-load check only fires when the persisted position has no
- // overlap with any visible viewport area.
+ // Manual reset snaps unconditionally; on-load check only fires when the
+ // stored position has no overlap with any visible viewport.
if (RequestPositionReset)
{
RequestPositionReset = false;
@@ -684,11 +669,8 @@ public sealed class ChatLogWindow : Window
if (IsChatMode && Plugin.InputPreview.IsDrawable)
Plugin.InputPreview.CalculatePreview();
- // Hellion Chat v0.6.1 — render the one-time hint banner first so it
- // sits above the tab area / sidebar in full window width. ImGui's
- // GetContentRegionAvail subtracts its height automatically because the
- // cursor advances past it before the message log calls
- // GetRemainingHeightForMessageLog, so we don't track the height here.
+ // Render the hint banner first so it sits above the tab area at full
+ // window width. ImGui accounts for its height automatically.
DrawV061HintBannerIfNeeded();
if (Plugin.Config.SidebarTabView)
@@ -713,8 +695,7 @@ public sealed class ChatLogWindow : Window
DrawChannelName(activeTab);
}
- // v1.0.2 — compute inputColour up front so the channel selector button
- // can also tint with it (existing input-text push remains below).
+ // inputColour computed up front so the channel selector button can share it.
var inputType = activeTab.CurrentChannel.UseTempChannel
? activeTab.CurrentChannel.TempChannel.ToChatType()
: activeTab.CurrentChannel.Channel.ToChatType();
@@ -1032,11 +1013,8 @@ public sealed class ChatLogWindow : Window
}
else
{
- // We cannot lookup ExtraChat channel names from index over
- // IPC so we just don't show the name if it's the tabs channel.
- //
- // We don't call channel.ToChatType().Name() as it has the
- // long name as used in the settings window.
+ // ExtraChat channel names aren't available over IPC by index,
+ // so we skip the name lookup and show the short form instead.
channelNameChunks =
[
new TextChunk(
@@ -1122,8 +1100,8 @@ public sealed class ChatLogWindow : Window
Plugin.CurrentTab.CurrentChannel.TempTellTarget = null;
}
- // Instead of calling SetChannel(), we ask the ExtraChat plugin to set a
- // channel override by just calling the command directly.
+ // ExtraChat linkshell channel switch: call the prefix command through the
+ // game chat because ExtraChat only registers stub handlers in Dalamud.
if (channel.Value.IsExtraChatLinkshell())
{
// Check that the command is registered in Dalamud so the game code
@@ -1169,10 +1147,8 @@ public sealed class ChatLogWindow : Window
];
}
- // v0.6.0 — pop-out windows route submission through this wrapper.
- // The main-window Chat buffer is briefly used as a vehicle for
- // SendChatBox (which reads it directly) and restored afterwards so
- // the main window does not visibly lose any half-typed input.
+ // Pop-out windows route submission here. The main Chat buffer is briefly
+ // used as a vehicle for SendChatBox and restored afterwards.
internal void SendChatBoxFromExternal(Tab tab, string text)
{
var saved = Chat;
@@ -1217,7 +1193,7 @@ public sealed class ChatLogWindow : Window
?? activeTab.CurrentChannel.TellTarget;
if (target != null)
{
- // ContentId 0 is a case where we can't directly send messages, so we send a /tell formatted message and let the game handle it
+ // ContentId 0: can't send directly, so format as /tell and let the game handle it.
if (target.ContentId == 0)
{
trimmed = $"/tell {target.ToTargetString()} {trimmed}";
@@ -1383,8 +1359,8 @@ public sealed class ChatLogWindow : Window
var maxLines = Plugin.Config.MaxLinesToRender;
var startLine = messages.Count > maxLines ? messages.Count - maxLines : 0;
- // Card-mode pre-loop hoist: theme/drawList/winLeft/winRight/border
- // are invariant per DrawMessages call; only cursorY moves per row.
+ // Card-mode pre-loop: theme/drawList/winLeft/winRight/border are invariant
+ // per DrawMessages call; only cursorY moves per row.
var theme = Plugin.ThemeRegistry.Active;
var drawList = ImGui.GetWindowDrawList();
var winLeft = ImGui.GetWindowPos().X;
@@ -1541,11 +1517,9 @@ public sealed class ChatLogWindow : Window
var lineWidth = ImGui.GetContentRegionAvail().X;
- // v1.2.0 — Card-Rows als Default, Compact-Density als Opt-Out.
- // Card-Mode: Sender-Header in Channel-Color auf eigener Zeile,
- // dann Body, dann subtile Border-Bottom als Card-Trenner.
- // Compact-Mode: bisheriges Verhalten — Sender + Space + Content
- // auf einer Zeile via SameLine.
+ // v1.2.0 card mode: sender on its own line in channel color, then body,
+ // then a subtle border as a card separator.
+ // Compact mode: sender + space + content on one line via SameLine.
var useCard = !Plugin.Config.UseCompactDensity;
if (useCard)
{
@@ -1558,7 +1532,7 @@ public sealed class ChatLogWindow : Window
{
DrawChunks(message.Sender, true, handler, lineWidth);
}
- // KEIN SameLine — Body landet auf eigener Zeile.
+ // No SameLine — body renders on its own line.
}
// We need to draw something otherwise the item visibility check below won't work.
@@ -1572,8 +1546,7 @@ public sealed class ChatLogWindow : Window
else
DrawChunks(message.Content, true, handler, lineWidth);
- // Subtile Border-Bottom als Card-Trenner. Border-Farbe mit
- // reduzierter Alpha (RGBA → 0x33) für dezente Trennung.
+ // Border bottom as card separator. Alpha reduced to 0x33 for subtlety.
{
var rowEndY = ImGui.GetCursorScreenPos().Y;
drawList.AddLine(
@@ -1646,9 +1619,8 @@ public sealed class ChatLogWindow : Window
if (!tabItem.Success)
continue;
- // v1.2.0 — Active-Tab-Underline-Pill (2 px Akzent statt Background-Fill).
- // Bewusst direkt nach TabItem-Setup; GetItemRectMin/Max referenziert noch
- // das Tab. ImGui hat keine native Underline-API, daher direkter DrawList-Pass.
+ // Active-tab underline pill (2px accent). No native ImGui underline API,
+ // so we use a direct DrawList pass.
{
var theme = Plugin.ThemeRegistry.Active;
var min = ImGui.GetItemRectMin();
@@ -1680,7 +1652,7 @@ public sealed class ChatLogWindow : Window
private void DrawTabSidebar()
{
var currentTab = -1;
- // v1.2.0 — Sidebar fix 44 px, kein Resize. Mehr Platz fürs Chat-Log.
+ // Sidebar fixed at 44px, no resize.
using var tabTable = ImRaii.Table(
"tabs-table",
2,
@@ -1696,28 +1668,19 @@ public sealed class ChatLogWindow : Window
var hasTabSwitched = false;
var childHeight = GetRemainingHeightForMessageLog();
- // v1.2.0 — Sidebar-Child ohne Theme-ChildBg, sonst füllt das
- // bläuliche Frame-Rect auch den oberen HeaderToolbar-Padding-Bereich
- // aus (sieht aus wie ein angeschnittener Block oberhalb der Buttons).
- // Vertikale Trennung zur Message-Spalte bleibt durch BordersInnerV
- // der Tab-Table erhalten.
+ // Sidebar child without ChildBg tint to avoid a colored block above the
+ // header toolbar area. Vertical separation is handled by BordersInnerV.
using (ImRaii.PushColor(ImGuiCol.ChildBg, 0u))
using (var child = ImRaii.Child("##chat2-tab-sidebar", new Vector2(-1, childHeight)))
{
if (child)
{
- // v1.2.0 — Top-Padding spiegelt die HeaderToolbar-Höhe der
- // rechten Spalte (DrawChatHeaderToolbar wird dort als erstes
- // gerendert, eine Frame-Zeile + ItemSpacing). Ohne diesen
- // Padding würden die Sidebar-Buttons oben am Window-Top
- // kleben, während die Messages erst unter der Toolbar
- // beginnen — vertikales Mismatch.
+ // Top padding mirrors the HeaderToolbar height so sidebar buttons
+ // align with the message log start.
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
var previousTab = Plugin.CurrentTab;
- // Hellion Chat — auto-tell-tabs section divider rendered
- // exactly once before the first temp tab, with a live unit
- // counter pulled directly from the tab list.
+ // Divider rendered once before the first temp tab with a live unit counter.
var tempTabHeaderRendered = false;
var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab);
@@ -1752,11 +1715,8 @@ public sealed class ChatLogWindow : Window
if (showGreetedAffordance)
{
- // Greeted toggle sits left of the selectable so the
- // click areas stay separate. The icon also doubles
- // as the visual "I'm done with this person" cue.
- // Compact frame padding keeps the icon dezent next
- // to the tab name instead of a chunky button block.
+ // Greeted toggle left of the selectable to keep click areas separate.
+ // Compact padding keeps the icon next to the tab name.
var greetedIcon = tab.IsGreeted
? FontAwesomeIcon.CheckCircle
: FontAwesomeIcon.Check;
@@ -1784,10 +1744,8 @@ public sealed class ChatLogWindow : Window
ImGui.SameLine();
}
- // v1.2.0 — Icon-only Sidebar mit Tooltip beim Hover.
- // Active-Tab kriegt Akzent-Color am Icon, Greeted-Tabs
- // werden auf TextDim gedimmt (löst den alten Header-
- // Dim-Trick ab, da wir keine Selectable mehr nutzen).
+ // Icon-only sidebar with tooltip on hover. Active tab gets accent color;
+ // greeted tabs are dimmed; tell tabs get a hash-based tint.
var theme = Plugin.ThemeRegistry.Active;
var icon = TabIconMapping.Resolve(tab);
uint iconColor;
@@ -1801,8 +1759,8 @@ public sealed class ChatLogWindow : Window
}
else if (tab.IsTempTab && tab.TellTarget != null && tab.TellTarget.IsSet())
{
- // v1.2.0 — Hash-Color-Tint differenziert parallele Auto-Tell-Tabs
- // visuell ohne dass User pro Tab manuell ein Custom-Icon setzen muss.
+ // Hash-based color tint differentiates parallel Auto-Tell tabs
+ // without requiring manual icon assignment per tab.
iconColor = TabTintCache.GetTint(tab);
}
else
@@ -1835,9 +1793,8 @@ public sealed class ChatLogWindow : Window
if (isCurrentTab)
{
- // v1.2.0 — Vertikale Akzent-Pill an der linken Window-Kante.
- // 3 px breit, halbe Tab-Höhe, vertikal zentriert. ImGui hat keine
- // native Pill-API, daher direkter DrawList-Pass.
+ // Vertical accent pill on the left window edge, 3px wide, half tab height,
+ // vertically centered. Direct DrawList pass, no native ImGui API for this.
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
const float pillWidth = 3f;
@@ -1853,10 +1810,8 @@ public sealed class ChatLogWindow : Window
); // leichter Rounding
}
- // v1.2.0 — Unread-Dot oben rechts am Icon. Sichtbar ohne Hover, damit
- // User Tabs mit ungelesenen Messages sofort erkennt. Aktive Tabs haben
- // per Konvention Unread = 0 (LastTab-Branch in ChatLogWindow), daher
- // kollidiert der Dot nicht mit der Active-Pill.
+ // Unread dot top-right of the icon. Active tabs have Unread=0 by convention
+ // so the dot never conflicts with the active pill.
if (!isCurrentTab && tab.UnreadMode != UnreadMode.None && tab.Unread > 0)
{
var min = ImGui.GetItemRectMin();
@@ -1868,10 +1823,7 @@ public sealed class ChatLogWindow : Window
min.Y + dotRadius + dotPadding
);
- // v1.2.0 — Sanfter Pulse-Effekt: Alpha schwankt zwischen 60% und
- // 100% mit ~2-Sekunden-Cycle (subtil, nicht hektisch).
- // Plugin.Config.ReduceMotion (Field seit v1.1.0) skipt den Pulse
- // und rendert statisch — Default ist Animation an.
+ // Sin-based 2s pulse: alpha oscillates 60-100%. Skipped when ReduceMotion is on.
var dotColor = theme.Colors.StatusDanger;
if (!Plugin.Config.ReduceMotion)
{
@@ -1941,14 +1893,8 @@ public sealed class ChatLogWindow : Window
Plugin.WantedTab = null;
}
- // Hellion Chat v0.6.1 — visible pop-out trigger right above the message
- // log so users discover the feature without having to right-click the tab.
- // Renders only for the active tab in the main ChatLogWindow; pop-out
- // windows have their own render path and skip this toolbar.
- //
- // Hellion Chat v1.3.0 also renders the optional Honorific title slot
- // left of the pop-out button, when HonorificService reports an active
- // custom title and the user has ShowHonorificTitleInHeader enabled.
+ // DrawChatHeaderToolbar: renders the pop-out button for the active tab.
+ // v1.3.0 also renders the optional Honorific title slot left of it.
private void DrawChatHeaderToolbar(Tab tab)
{
DrawHonorificTitleSlot();
@@ -1973,16 +1919,9 @@ public sealed class ChatLogWindow : Window
}
}
- // Renders the Honorific custom title to the left of the pop-out button,
- // wrapped in guillemets to match how the game itself displays titles.
- // We lay out the title first, then DrawPopOutButton uses
- // GetContentRegionAvail to anchor itself flush right, which is why the
- // call order in DrawChatHeaderToolbar matters: title first, button second.
- //
- // The slot stays on the same line as the pop-out button so the chat
- // log doesn't lose vertical space; we use ImGui.SameLine after our
- // text so the cursor X is still on the toolbar row when the pop-out
- // button takes over.
+ // Title rendered first so DrawPopOutButton can anchor flush right via
+ // GetContentRegionAvail. Call order in DrawChatHeaderToolbar matters.
+ // SameLine keeps both on the same toolbar row.
private void DrawHonorificTitleSlot()
{
var service = Plugin.HonorificService;
@@ -2028,8 +1967,7 @@ public sealed class ChatLogWindow : Window
var theme = Plugin.ThemeRegistry.Active;
- // Group so the tooltip's IsItemHovered check fires for hover anywhere
- // on the crown-plus-title pair, not just one of the two.
+ // Group so IsItemHovered covers both the crown icon and the title text.
ImGui.BeginGroup();
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
using (Plugin.FontManager.FontAwesome.Push())
@@ -2051,11 +1989,7 @@ public sealed class ChatLogWindow : Window
ImGui.SameLine();
}
- // Hellion Chat v0.6.1 — One-Time-Hint-Banner introducing the chat header
- // pop-out toolbar button and the right-click pathway. Reuses the visual
- // pattern from Popout.cs DrawHintBannerIfNeeded so users see a familiar
- // dismiss-affordance. Returns the vertical space the banner consumed
- // (0 when not shown) so the message log can shrink accordingly.
+ // One-time hint banner for the pop-out header button and right-click pathway.
private float DrawV061HintBannerIfNeeded()
{
if (Plugin.Config.SeenPopOutHeaderHint)
@@ -2070,10 +2004,7 @@ public sealed class ChatLogWindow : Window
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
var dismiss = false;
var openSettings = false;
- // RAII for the style stack so an early return in this block
- // (or a later refactor that introduces one) can never leave the
- // ImGui style stack unbalanced. Matches the convention used
- // elsewhere in this file.
+ // RAII style stack so an early return can never leave ImGui unbalanced.
using (ImRaii.PushColor(ImGuiCol.ChildBg, bg))
using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1f))
using (
@@ -2176,10 +2107,8 @@ public sealed class ChatLogWindow : Window
internal readonly List PopOutDocked = [];
internal readonly HashSet PopOutWindows = [];
- // v0.6.0 — live enumeration of all active Popout windows so the
- // KeybindManager can find a focused ChatInputBar to forward tab-cycle
- // keybinds to. Filter on IsOpen prevents touching closed-but-still-
- // registered popouts.
+ // Live enumeration of active Popout windows for KeybindManager tab-cycle forwarding.
+ // Filters on IsOpen to skip closed-but-registered popouts.
internal IEnumerable ActivePopouts =>
Plugin.WindowSystem.Windows.OfType().Where(p => p.IsOpen);
@@ -2352,8 +2281,7 @@ public sealed class ChatLogWindow : Window
}
finally
{
- // ImGuiListClipperPtr wraps an unmanaged ImGuiListClipper allocated above.
- // Without Destroy() the unmanaged block leaks per autocomplete render.
+ // Destroy frees the unmanaged ImGuiListClipper allocated above; without it the block leaks per render.
clipper.Destroy();
}
}
@@ -2687,9 +2615,8 @@ public sealed class ChatLogWindow : Window
return $"Player {hashCode:X8}";
}
- // Snap threshold in pixels: at least this much of the window must overlap
- // a visible viewport so the user can still grab the first tab header.
- // Below the threshold the window is considered off-screen.
+ // Snap threshold: minimum window overlap with a visible viewport before
+ // we consider it off-screen.
private const int OnScreenMinOverlapX = 100;
private const int OnScreenMinOverlapY = 40;
@@ -2725,9 +2652,7 @@ public sealed class ChatLogWindow : Window
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
);
- // Pop-outs are intentionally non-persistent (cleared on plugin reload),
- // so an off-screen pop-out can never survive a session boundary. The
- // main window above is the only persistence target that needs an
- // explicit recovery path.
+ // Pop-outs don't persist across sessions so they can never end up off-screen
+ // after a reload. Only the main window needs explicit recovery.
}
}
diff --git a/HellionChat/Ui/DbViewer.cs b/HellionChat/Ui/DbViewer.cs
index a78e55f..58b2c3c 100644
--- a/HellionChat/Ui/DbViewer.cs
+++ b/HellionChat/Ui/DbViewer.cs
@@ -211,13 +211,6 @@ public class DbViewer : Window
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(Language.Export_Txt_Tooltip);
- // Hellion Chat: the JSON export button used to dump the database in
- // the upstream webinterface's wire format. With the webinterface
- // removed there is no consumer for that format any more, so the
- // button is dropped. The Privacy tab's MessageExporter covers the
- // same ground (Markdown / JSON / CSV) with channel and date filters
- // and is the supported way to get history out of the plugin.
-
var width = 350 * ImGuiHelpers.GlobalScale;
var loadingIndicator = IsProcessing && ProcessingStart < Environment.TickCount64;
diff --git a/HellionChat/Ui/HellionStyle.cs b/HellionChat/Ui/HellionStyle.cs
index 04d7689..4d0774a 100644
--- a/HellionChat/Ui/HellionStyle.cs
+++ b/HellionChat/Ui/HellionStyle.cs
@@ -5,18 +5,12 @@ using HellionChat.Util;
namespace HellionChat.Ui;
-///
-/// ImGui style override for Hellion Chat. v1.1.0 ist die Engine
-/// theme-getrieben: PushGlobal nimmt eine Theme-Instance + Window-
-/// Opacity, die gesamten Color- und Style-Slots werden aus dem Theme
-/// gelesen statt aus einer fixen Konstanten-Tabelle.
-///
+// Theme-driven ImGui style override. PushGlobal is pushed once per frame
+// in Plugin.Draw and drives every Hellion-rendered window.
internal static class HellionStyle
{
- ///
- /// Local color stack auf Basis des aktiven Themes. Cheap. Use inside a
- /// `using var _ = HellionStyle.Push(theme);` block.
- ///
+ // Local color stack for the active theme. Use inside a
+ // `using var _ = HellionStyle.Push(theme);` block.
internal static IDisposable Push(Theme theme)
{
var a = theme.AbgrCache;
@@ -37,13 +31,8 @@ internal static class HellionStyle
return stack;
}
- ///
- /// Global color and style-variable stack pushed once per frame in
- /// Plugin.Draw. Drives every Hellion-rendered window from the active
- /// theme's palette and layout values.
- ///
- /// Active theme from ThemeRegistry.
- /// Window background alpha (0.5–1.0).
+ // Global color and style stack pushed once per frame.
+ // windowOpacity: window background alpha (0.5-1.0).
internal static IDisposable PushGlobal(Theme theme, float windowOpacity = 1.0f)
{
var c = theme.Colors;
@@ -54,15 +43,11 @@ internal static class HellionStyle
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
- // ChildBg-Alpha: Sub-Bereiche (Tab-Sidebar, Message-Area, Input-Bar)
- // werden im ChatLog-Window als BeginChild gezeichnet. Würde der ChildBg
- // mit dem gleichen Alpha wie WindowBg gerendert, multiplizieren sich
- // die Layer (1 - (1-α)² Deckung), und 50 % WindowOpacity kommt mit
- // 75 % Deckung im Child-Bereich an — das Fenster wirkt solider als der
- // Slider verspricht. Bei voller Opacity bleibt der Theme-Akzent
- // erhalten (Theme-eigene Alpha-Komponente, i.d.R. FF); sobald der User
- // Transparenz zieht, wird ChildBg vollständig durchsichtig damit nur
- // der WindowBg-Layer die finale Deckung bestimmt.
+ // ChildBg alpha: child areas rendered inside ChatLogWindow would
+ // multiply their alpha with WindowBg, making 50% opacity appear
+ // ~75% solid. At full opacity the theme's alpha is preserved; below
+ // it ChildBg goes fully transparent so only WindowBg sets the final
+ // coverage.
var childBgAlpha = windowOpacity >= 0.999f ? (c.ChildBg & 0xFFu) : 0u;
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | childBgAlpha;
@@ -77,8 +62,8 @@ internal static class HellionStyle
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize);
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize);
- // Surfaces — WindowBg/ChildBg use the per-push opacity-modulated value,
- // so they go through the RGBA path; everything else reads from cache.
+ // Surfaces — WindowBg/ChildBg use opacity-modulated values (RGBA path);
+ // everything else reads from the pre-computed ABGR cache.
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
stack.PushColorAbgr(ImGuiCol.PopupBg, a.ChildBg);
diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs
index 5a20839..fbbe483 100644
--- a/HellionChat/Ui/Popout.cs
+++ b/HellionChat/Ui/Popout.cs
@@ -12,19 +12,15 @@ internal class Popout : Window
private readonly Tab Tab;
private readonly int Idx;
- private long FrameTime; // set every frame
+ private long FrameTime;
private long LastActivityTime = Environment.TickCount64;
- // v0.6.0 — optional input bar inside the pop-out window. Lazy-allocated
- // when the user enables Tab.PopOutInputEnabled and torn down when the
- // toggle is turned off (independent text buffer is intentionally
- // discarded — see v0.6.0 spec edge-case P1).
+ // Optional input bar inside the pop-out. Lazy-allocated when enabled,
+ // torn down on toggle-off (buffer discarded intentionally).
public ChatInputBar? InputBar { get; private set; }
public bool HasFocusedInputBar => InputBar?.IsFocused ?? false;
- // Hellion Chat — v0.6.1 expose just the tab identifier (not the whole Tab
- // reference) so AutoTellTabsService.DropOldestTempTab can locate the
- // matching pop-out window when an LRU temp tab gets evicted.
+ // Exposed so AutoTellTabsService can locate this window during LRU eviction.
internal Guid TabIdentifier => Tab.Identifier;
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx)
@@ -40,12 +36,9 @@ internal class Popout : Window
IsOpen = true;
RespectCloseHotkey = false;
DisableWindowSounds = true;
- // v1.2.1 — KEIN AllowBackgroundBlur. Pop-Outs werden vom User häufig
- // im Dalamud-Tab-Container mit anderen Plugin-Windows kombiniert; in
- // dem Render-Pfad blurt Dalamud den gesamten Container, nicht nur
- // das Pop-Out — würde die Tab-Bar oben und benachbarte Plugins
- // mitziehen. Wer Blur in Pop-Outs will, kann ihn via Dalamud-
- // Hamburger-Menü pro Window selbst aktivieren.
+ // AllowBackgroundBlur is intentionally off: Dalamud blurs the entire
+ // tab container, not just this window, which would affect adjacent plugins.
+ // Users can enable blur per-window via the Dalamud hamburger menu.
}
public override void PreOpenCheck()
@@ -70,7 +63,6 @@ internal class Popout : Window
return true;
}
- // Activity in the tab, this popout window, or the main chat log window.
var lastActivityTime = Math.Max(Tab.LastActivity, LastActivityTime);
lastActivityTime = Math.Max(lastActivityTime, ChatLogWindow.LastActivityTime);
return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout;
@@ -78,10 +70,8 @@ internal class Popout : Window
public override void PreDraw()
{
- // Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein
- // zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw
- // pusht das aktive Hellion-Theme global; Pop-Out zeichnet sich damit
- // konsistent zum Haupt-Chat-Window.
+ // Theme engine pushes the active theme globally in Plugin.Draw;
+ // pop-outs draw consistently without per-window overrides.
Flags = ImGuiWindowFlags.None;
if (!Plugin.Config.ShowPopOutTitleBar)
Flags |= ImGuiWindowFlags.NoTitleBar;
@@ -92,19 +82,10 @@ internal class Popout : Window
if (!Tab.CanResize)
Flags |= ImGuiWindowFlags.NoResize;
- // Idx may point past the end if PopOutDocked was resized (e.g., a tab
- // dropped) between the AddPopOutsToDraw() snapshot and this frame.
- // Guard the read so we don't index into stale state.
+ // Guard against Idx pointing past the end if PopOutDocked was resized mid-frame.
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count && !ChatLogWindow.PopOutDocked[Idx])
{
- if (Tab.IndependentOpacity)
- {
- BgAlpha = Tab.Opacity / 100f;
- }
- else
- {
- BgAlpha = Plugin.Config.WindowOpacity;
- }
+ BgAlpha = Tab.IndependentOpacity ? Tab.Opacity / 100f : Plugin.Config.WindowOpacity;
}
}
@@ -118,24 +99,15 @@ internal class Popout : Window
ImGui.Separator();
}
- // v0.6.0 — one-time hint banner explaining the new pop-out input
- // feature. Shown once per user; "Got it" or "Open settings"
- // dismisses it and persists the flag.
var hintBannerHeight = DrawHintBannerIfNeeded();
- // v0.6.0 — pop-out optional input bar. Reserve height first so the
- // message log draws into the right region; only shown when the
- // global master switch is on. Toggle-OFF resets InputBar so the
- // next toggle-ON gives a fresh buffer (no stale text persists).
+ // Toggle-OFF resets InputBar so the next toggle-ON starts with a fresh buffer.
var inputEnabled = Plugin.Config.PopOutInputEnabled;
if (!inputEnabled && InputBar != null)
- {
InputBar = null;
- }
+
if (inputEnabled)
- {
InputBar ??= new ChatInputBar(ChatLogWindow.Plugin, ChatLogWindow, () => Tab);
- }
var inputBarHeight = inputEnabled
? ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y
@@ -155,8 +127,7 @@ internal class Popout : Window
LastActivityTime = FrameTime;
}
- // Returns the vertical space the banner consumed (0 when not shown)
- // so the message log can shrink accordingly.
+ // Returns the vertical space consumed by the banner (0 when not shown).
private float DrawHintBannerIfNeeded()
{
if (Plugin.Config.SeenPopOutInputHint)
@@ -240,21 +211,18 @@ internal class Popout : Window
private bool HideStateCheck()
{
- // if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
{
CurrentHideState = HideState.Battle;
- Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Battle");
+ Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle");
}
- // If the chat is hidden because of battle, we reset it here
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
{
CurrentHideState = HideState.None;
- Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle → None");
+ Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
}
- // if the chat has no hide state and in a cutscene, set the hide state to cutscene
if (
Tab.HideDuringCutscenes
&& CurrentHideState == HideState.None
@@ -264,11 +232,10 @@ internal class Popout : Window
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
{
CurrentHideState = HideState.Cutscene;
- Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Cutscene");
+ Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene");
}
}
- // if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
if (
CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride
&& !Plugin.CutsceneActive
@@ -276,25 +243,23 @@ internal class Popout : Window
)
{
Plugin.Log.Verbose(
- $"Popout HideState [{Tab.Name}]: {CurrentHideState} → None (cutscene/gpose ended)"
+ $"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
);
CurrentHideState = HideState.None;
}
- // if the chat is hidden because of a cutscene and the chat has been activated, show chat
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
{
CurrentHideState = HideState.CutsceneOverride;
Plugin.Log.Verbose(
- $"Popout HideState [{Tab.Name}]: Cutscene → CutsceneOverride (user activate)"
+ $"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
);
}
- // if the user hid the chat and is now activating chat, reset the hide state
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
{
CurrentHideState = HideState.None;
- Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User → None (activate)");
+ Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)");
}
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs
index 4d62d53..d9aad7c 100755
--- a/HellionChat/Ui/Settings.cs
+++ b/HellionChat/Ui/Settings.cs
@@ -92,10 +92,8 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
View = SettingsView.Overview;
}
- // ESC im Detail-View kehrt zur Overview zurück. Window-Focus-Check ist
- // Pflicht — sonst triggert ESC auch wenn der User ein anderes Fenster
- // fokussiert hat und ESC fürs Game-Menü drückt (Codebase-Pattern siehe
- // Util/SearchSelector.cs:37).
+ // ESC in Detail view returns to Overview. Window focus check is
+ // required so ESC doesn't fire when the user targets a different window.
if (
View == SettingsView.Detail
&& ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)
@@ -128,13 +126,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
private void DrawDetail()
{
- // Breadcrumb-Header — Akzent-Cyan, klickbar, führt zurück zur Overview
+ // Breadcrumb header -- accent cyan, clickable, returns to Overview.
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF00BED2u))
using (ImRaii.PushColor(ImGuiCol.Button, 0u))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, 0x33FFFFFFu))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, 0x55FFFFFFu))
{
- if (ImGui.SmallButton("← Settings"))
+ if (ImGui.SmallButton("<- Settings"))
{
View = SettingsView.Overview;
return;
@@ -149,11 +147,8 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
ImGui.Separator();
ImGui.Spacing();
- // Section-Content in voller Breite. Die Tab-Liste links ist überholt:
- // der User ist bereits über die Card-Übersicht navigiert, eine zweite
- // Tab-Liste daneben würde nur den Vanilla-Look zurückbringen. Falls
- // der User in eine andere Section will, geht er zurück zur Overview
- // (Breadcrumb / ESC).
+ // Section content fills full width. Navigation back to another
+ // section goes via the breadcrumb or ESC.
var style = ImGui.GetStyle();
var height =
ImGui.GetContentRegionAvail().Y
@@ -182,9 +177,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
ImGui.SameLine();
if (ImGui.Button(Language.Settings_Discard))
- {
IsOpen = false;
- }
const string buttonLabel = "Anna's Ko-fi";
const string buttonLabel2 = "Infi's Ko-fi";
@@ -217,7 +210,6 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
if (!save)
return;
- // calculate all conditions before updating config
var hideChanged = !Mutable.HideChat && Mutable.HideChat != Plugin.Config.HideChat;
var languageChanged = Mutable.LanguageOverride != Plugin.Config.LanguageOverride;
var fontChanged =
@@ -230,18 +222,16 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
var italicStateChanged = Mutable.ItalicEnabled != Plugin.Config.ItalicEnabled;
- // v1.2.0 — Refilter only if a filter-relevant setting actually
- // changed. The Clear+Refilter cycle reloads messages from the DB,
- // which silently wipes any in-session message that wasn't
- // persisted (Privacy-First config blocks most channels from DB).
- // Cosmetic changes (theme, tab icons, layout flags) trigger no
- // refilter — chat history stays intact.
+
+ // Only refilter when filter-relevant settings changed. Clear+Refilter
+ // reloads from the DB and silently drops in-session messages that
+ // weren't persisted (Privacy-First blocks most channels). Cosmetic
+ // changes (theme, icons, layout) skip the cycle.
var filtersChanged = HasFilterRelevantChanges();
Plugin.Config.UpdateFrom(Mutable, true);
- // save after 60 frames have passed, which should hopefully not
- // commit any changes that cause a crash
+ // Defer save by 60 frames to avoid committing changes that cause a crash.
Plugin.DeferredSaveFrames = 60;
if (filtersChanged)
{
@@ -259,24 +249,19 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
GameFunctions.GameFunctions.SetChatInteractable(true);
if (Plugin.Config.ShowEmotes)
- _ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
+ _ = EmoteCache.LoadData();
Initialise();
}
///
- /// v1.2.0 — Detects whether any setting that influences message
- /// filtering changed between Plugin.Config and the Mutable working
- /// copy. Used to gate the heavy ClearAllTabs+FilterAllTabsAsync cycle
- /// in Save: cosmetic changes (theme, tab icons, layout flags) do not
- /// touch the chat log, only filter-relevant changes do. Without this
- /// gate, every settings save wipes the chat history of any channel
- /// the Privacy filter blocks from being persisted to the DB —
- /// reported by Flo from in-game testing 2026-05-05/06.
+ /// Returns true if any setting that influences message filtering changed
+ /// between Plugin.Config and the Mutable working copy. Gates the heavy
+ /// ClearAllTabs+FilterAllTabsAsync cycle on Save so cosmetic changes
+ /// don't wipe in-session chat history.
///
private bool HasFilterRelevantChanges()
{
- // Top-level privacy controls.
if (Mutable.PrivacyFilterEnabled != Plugin.Config.PrivacyFilterEnabled)
return true;
if (Mutable.PrivacyPersistUnknownChannels != Plugin.Config.PrivacyPersistUnknownChannels)
@@ -285,27 +270,23 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
return true;
// FilterIncludePreviousSessions changes the GetMostRecentMessages
- // window in MessageManager.FilterAllTabs and is therefore filter-
- // relevant even though it lives outside the Privacy block.
+ // window and is filter-relevant even outside the Privacy block.
if (Mutable.FilterIncludePreviousSessions != Plugin.Config.FilterIncludePreviousSessions)
return true;
- // Per-tab channel selection. Compare persistent tabs only —
- // TempTabs are session-only and never refiltered anyway.
+ // Compare persistent tabs only -- TempTabs are never refiltered.
var origPersistent = Plugin.Config.Tabs.Where(t => !t.IsTempTab).ToList();
var newPersistent = Mutable.Tabs.Where(t => !t.IsTempTab).ToList();
if (origPersistent.Count != newPersistent.Count)
- return true; // add or delete
+ return true;
for (var i = 0; i < origPersistent.Count; i++)
{
var orig = origPersistent[i];
var neu = newPersistent[i];
- // Identifier mismatch at the same index means reorder or
- // a slot got swapped — treat as filter-relevant so the new
- // channel-selection layout actually applies.
+ // Identifier mismatch means reorder or slot swap -- treat as filter-relevant.
if (orig.Identifier != neu.Identifier)
return true;
@@ -314,8 +295,6 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
if (!orig.ExtraChatChannels.SetEquals(neu.ExtraChatChannels))
return true;
- // SelectedChannels is a Dictionary
- // — value-tuple equality already does the right thing per-pair.
if (orig.SelectedChannels.Count != neu.SelectedChannels.Count)
return true;
foreach (var pair in orig.SelectedChannels)
diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs
index 4026a95..1569f2c 100644
--- a/HellionChat/Ui/SettingsOverview.cs
+++ b/HellionChat/Ui/SettingsOverview.cs
@@ -11,11 +11,7 @@ internal sealed class SettingsOverview
{
private readonly SettingsWindow _window;
- // Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow.
- // v1.2.1: Cards thematisch re-sortiert. Theme & Layout vereint Theme-
- // Picker + Frame-Style + Timestamps; Fonts & Colours vereint Schriften
- // + Chat-Farben; Data Management vereint Storage + Retention + Cleanup
- // + Export + DB-Viewer + Advanced.
+ // Card order matches the Tabs index in SettingsWindow 1:1.
private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs =
[
(FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"),
@@ -64,9 +60,7 @@ internal sealed class SettingsOverview
var avail = ImGui.GetContentRegionAvail();
var columns = avail.X >= 700f ? 3 : 2;
var cardWidth = (avail.X - (columns - 1) * 8f) / columns;
- // v1.2.1 — Subtexte wrappen jetzt auf zwei Zeilen, daher 110f statt der
- // v1.1.0-Höhe 96f. Wrap-Breite + Y-Position der Subtext-Zeile sind in
- // DrawCard auf den Card-Innenrand abgestimmt.
+ // 110f accommodates two-line subtexts; wrap width is matched in DrawCard.
var cardHeight = 110f;
for (var i = 0; i < CardDefs.Length; i++)
@@ -90,9 +84,8 @@ internal sealed class SettingsOverview
float h
)
{
- // BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item.
- // Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die
- // einzelnen InvisibleButton/Text-Items separat und das Wrapping bricht.
+ // BeginGroup makes the card a single layout item so SameLine works
+ // in the caller loop -- without it ImGui tracks each child separately.
ImGui.BeginGroup();
var cursorBefore = ImGui.GetCursorScreenPos();
@@ -103,9 +96,6 @@ internal sealed class SettingsOverview
var draw = ImGui.GetWindowDrawList();
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
- // Inhalts-Overlay: Icon + Title via DrawList (kein Wrap nötig). Subtext
- // läuft über ImGui-Cursor + PushTextWrapPos damit der Text bei
- // Card-Innenbreite umbricht statt rechts geclippt zu werden.
var iconPos = cursorBefore + new Vector2(16f, 12f);
var titlePos = cursorBefore + new Vector2(16f, 40f);
var subtextPos = cursorBefore + new Vector2(16f, 62f);
@@ -120,10 +110,8 @@ internal sealed class SettingsOverview
draw.AddText(titlePos, titleColor, title);
- // Subtext mit Wrap auf Card-Innenbreite (16 px Padding links + rechts).
- // Cursor-basiertes TextUnformatted würde die ImGui-Group-Bounds
- // erweitern und das SameLine-Wrapping in der Card-Reihe brechen, daher
- // bleibt der Subtext bewusst beim DrawList-Overlay-Pattern.
+ // Subtext wraps at card inner width (16px padding each side) via DrawList
+ // to avoid expanding the group bounds and breaking SameLine in the card row.
var subtextWrapWidth = w - 32f;
draw.AddText(
ImGui.GetFont(),
@@ -137,8 +125,6 @@ internal sealed class SettingsOverview
ImGui.EndGroup();
if (clicked)
- {
_window.OpenSection(index);
- }
}
}
diff --git a/HellionChat/Ui/SettingsTabs/Chat.cs b/HellionChat/Ui/SettingsTabs/Chat.cs
index 1ab7078..d4ea21e 100644
--- a/HellionChat/Ui/SettingsTabs/Chat.cs
+++ b/HellionChat/Ui/SettingsTabs/Chat.cs
@@ -9,10 +9,7 @@ using HellionChat.Util;
namespace HellionChat.Ui.SettingsTabs;
-// Chat-Tab — vier eigenständige Sektionen: Auto-Tell-Tabs, Behaviour,
-// Preview, Emotes. Der Emotes-Block ist 1:1 aus der Bestand-Datei
-// Emote.cs übernommen; die Datei wird in Plan-Task 11 (Settings UX
-// Polish v0.5.0) entfernt, sobald alle Tabs migriert sind.
+// Four sections: Auto-Tell Tabs, Behaviour, Preview, Emotes.
internal sealed class Chat : ISettingsTab
{
private Plugin Plugin { get; }
@@ -22,9 +19,8 @@ internal sealed class Chat : ISettingsTab
private SearchSelector.SelectorPopupOptions WordPopupOptions;
- // Snapshot of EmoteCache.State for which we last built WordPopupOptions.
- // Without this, an empty FilteredSheet (e.g., the user blocked every emote)
- // would trigger a refill every frame the settings tab is open.
+ // Tracks which EmoteCache state WordPopupOptions was built for so we
+ // don't refill every frame when FilteredSheet is empty.
private EmoteCache.LoadingState? WordPopupOptionsBuiltFor;
internal Chat(Plugin plugin, Configuration mutable)
@@ -36,15 +32,13 @@ internal sealed class Chat : ISettingsTab
WordPopupOptionsBuiltFor = EmoteCache.State;
}
- private SearchSelector.SelectorPopupOptions RefillSheet()
- {
- return new SearchSelector.SelectorPopupOptions
+ private SearchSelector.SelectorPopupOptions RefillSheet() =>
+ new SearchSelector.SelectorPopupOptions
{
FilteredSheet = EmoteCache
.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w))
.ToArray(),
};
- }
public void Draw(bool changed)
{
@@ -61,9 +55,7 @@ internal sealed class Chat : ISettingsTab
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_AutoTellTabs_Heading);
if (!tree.Success)
- {
return;
- }
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
@@ -76,9 +68,7 @@ internal sealed class Chat : ISettingsTab
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
var limit = Mutable.AutoTellTabsLimit;
if (ImGui.SliderInt(HellionStrings.ChatLog_AutoTellTabs_Limit_Name, ref limit, 1, 50))
- {
Mutable.AutoTellTabsLimit = limit;
- }
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Limit_Description);
ImGui.Checkbox(
@@ -119,9 +109,7 @@ internal sealed class Chat : ISettingsTab
100
)
)
- {
Mutable.AutoTellTabsHistoryPreload = preload;
- }
ImGuiUtil.HelpMarker(HellionStrings.Privacy_AutoTellTabs_Preload_Description);
ImGui.Spacing();
@@ -133,9 +121,7 @@ internal sealed class Chat : ISettingsTab
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Behaviour_Heading);
if (!tree.Success)
- {
return;
- }
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
@@ -160,9 +146,7 @@ internal sealed class Chat : ISettingsTab
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Preview_Heading);
if (!tree.Success)
- {
return;
- }
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
@@ -178,9 +162,7 @@ internal sealed class Chat : ISettingsTab
foreach (var position in Enum.GetValues())
{
if (ImGui.Selectable(position.Name(), Mutable.PreviewPosition == position))
- {
Mutable.PreviewPosition = position;
- }
}
}
}
@@ -193,9 +175,7 @@ internal sealed class Chat : ISettingsTab
ref Mutable.PreviewMinimum
)
)
- {
Mutable.PreviewMinimum = Math.Clamp(Mutable.PreviewMinimum, 1, 250);
- }
ImGui.Checkbox(Language.Options_PreviewOnlyIf_Name, ref Mutable.OnlyPreviewIf);
ImGuiUtil.HelpMarker(Language.Options_PreviewOnlyIf_Description);
@@ -206,9 +186,7 @@ internal sealed class Chat : ISettingsTab
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Emotes_Heading);
if (!tree.Success)
- {
return;
- }
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
@@ -233,17 +211,13 @@ internal sealed class Chat : ISettingsTab
using (Plugin.FontManager.FontAwesome.Push())
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
- // Open the selector popup on left-click; SelectorPopup uses
- // ImRaii.ContextPopupItem internally which only opens on right-
- // click otherwise — without this OpenPopup the button looked
- // active but the popup never appeared on a normal click.
+ // OpenPopup on click because SelectorPopup uses ContextPopupItem
+ // which only triggers on right-click by default.
if (ImGui.IsItemClicked())
ImGui.OpenPopup("WordAddPopup");
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
- {
Mutable.BlockedEmotes.Add(newWord);
- }
using (
var table = ImRaii.Table(
@@ -257,11 +231,9 @@ internal sealed class Chat : ISettingsTab
{
ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable);
ImGui.TableSetupColumn("##Del", ImGuiTableColumnFlags.WidthStretch, 0.07f);
-
ImGui.TableHeadersRow();
- var copiedList = Mutable.BlockedEmotes.ToArray();
- foreach (var word in copiedList)
+ foreach (var word in Mutable.BlockedEmotes.ToArray())
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(word);
@@ -274,9 +246,7 @@ internal sealed class Chat : ISettingsTab
!ImGui.GetIO().KeyCtrl
)
)
- {
Mutable.BlockedEmotes.Remove(word);
- }
}
}
}
@@ -289,17 +259,14 @@ internal sealed class Chat : ISettingsTab
ImGui.Spacing();
if (EmoteCache.State is EmoteCache.LoadingState.Done)
- {
ImGui.TextColored(ImGuiColors.HealerGreen, Language.Options_Emote_Ready);
- }
else
- {
ImGui.TextColored(ImGuiColors.DPSRed, Language.Options_Emote_NotReady);
- }
ImGui.TextUnformatted(
$"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}"
);
+
using (
var emoteTable = ImRaii.Table(
"##LoadedEmotes",
diff --git a/HellionChat/Ui/SettingsTabs/Information.cs b/HellionChat/Ui/SettingsTabs/Information.cs
index 4ed049c..2c9d52f 100644
--- a/HellionChat/Ui/SettingsTabs/Information.cs
+++ b/HellionChat/Ui/SettingsTabs/Information.cs
@@ -8,9 +8,7 @@ using HellionChat.Util;
namespace HellionChat.Ui.SettingsTabs;
-// Information-Tab vereint die früheren About- und Changelog-Tabs in
-// drei kollabierbaren Sektionen. Der About-Inhalt ist 1:1 aus About.cs
-// übernommen, die Changelog-Render-Logik aus Changelog.cs.
+// Combines the former About and Changelog tabs into three collapsible sections.
internal sealed class Information : ISettingsTab
{
private Configuration Mutable { get; }
diff --git a/HellionChat/Ui/SettingsTabs/Integrations.cs b/HellionChat/Ui/SettingsTabs/Integrations.cs
index eceb073..dba5bfb 100644
--- a/HellionChat/Ui/SettingsTabs/Integrations.cs
+++ b/HellionChat/Ui/SettingsTabs/Integrations.cs
@@ -8,9 +8,8 @@ using HellionChat.Util;
namespace HellionChat.Ui.SettingsTabs;
-// First settings tab introduced in v1.3.0 (Plugin Integrations Cycle 1).
-// Designed to grow organically: each future cycle adds a new section above
-// the "Coming soon" block and removes the corresponding stub item.
+// Added in v1.3.0. Each future integration cycle adds a section above
+// the "Coming soon" block and removes its stub item.
internal sealed class Integrations : ISettingsTab
{
private Plugin Plugin { get; }
@@ -48,11 +47,9 @@ internal sealed class Integrations : ISettingsTab
DrawHonorificStatus();
ImGui.Spacing();
- // The toggle is enabled regardless of detection state — leaving it
- // on means "render when available, hide otherwise". Disabling the
- // toggle when Honorific is missing would force the user to retoggle
- // it every time Honorific is reloaded, which is worse UX than the
- // silent auto-hide.
+ // Toggle works regardless of detection state: "show when available,
+ // hide otherwise". Disabling it when Honorific is missing would force
+ // the user to retoggle on every reload.
if (
ImGui.Checkbox(
HellionStrings.Settings_Integrations_Honorific_Toggle,
@@ -76,11 +73,9 @@ internal sealed class Integrations : ISettingsTab
}
}
- // Maintainer attribution. Honorific has no LICENSE in its repo so we
- // can't bundle its assets, but linking to the upstream and the
- // author's profile is the polite minimum. Plain ImGui buttons keep
- // the visual weight modest, the FontAwesome Brands subset is not
- // guaranteed in Dalamud's font set so we use text labels.
+ // Honorific has no LICENSE in its repo so we link upstream and author
+ // instead of bundling assets. Text labels because FA Brands isn't
+ // guaranteed in Dalamud's font set.
ImGui.Spacing();
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo))
{
@@ -147,9 +142,7 @@ internal sealed class Integrations : ISettingsTab
ImGui.TextWrapped(HellionStrings.Settings_Integrations_ComingSoon_Intro);
ImGui.Spacing();
- // Static list maintained in code (not Configuration). Each cycle
- // that lands a real integration removes its stub here and adds a
- // full section above the Coming Soon block.
+ // Each integration cycle removes its stub here and adds a full section above.
DrawComingSoonItem(
HellionStrings.Settings_Integrations_ComingSoon_ContextMenu_Title,
HellionStrings.Settings_Integrations_ComingSoon_ContextMenu_Description
diff --git a/HellionChat/Ui/SettingsTabs/Privacy.cs b/HellionChat/Ui/SettingsTabs/Privacy.cs
index 0924a01..27e70a4 100644
--- a/HellionChat/Ui/SettingsTabs/Privacy.cs
+++ b/HellionChat/Ui/SettingsTabs/Privacy.cs
@@ -20,8 +20,7 @@ internal sealed class Privacy : ISettingsTab
Mutable = mutable;
}
- // (HeadingKey lookup, ChatType list). Heading is resolved per-frame so
- // a runtime LanguageChanged call updates the labels immediately.
+ // (HeadingKey, ChatType list). Heading resolved per-frame for live language switching.
private static readonly (Func Heading, ChatType[] Types)[] Groups =
[
(
diff --git a/HellionChat/Ui/SettingsTabs/Tabs.cs b/HellionChat/Ui/SettingsTabs/Tabs.cs
index d3457c6..3f0948b 100755
--- a/HellionChat/Ui/SettingsTabs/Tabs.cs
+++ b/HellionChat/Ui/SettingsTabs/Tabs.cs
@@ -123,7 +123,7 @@ internal sealed class Tabs : ISettingsTab
ImGuiInputTextFlags.EnterReturnsTrue
);
- // v1.2.0 — Per-Tab Icon-Override. Default-Mapping greift falls nichts gesetzt.
+ // Per-tab icon override added in v1.2.0. Falls back to default mapping if unset.
ImGui.TextUnformatted(HellionStrings.Tabs_Icon_Label);
ImGui.SameLine();
ImGuiUtil.HelpMarker(HellionStrings.Tabs_Icon_HelpMarker);
@@ -135,7 +135,7 @@ internal sealed class Tabs : ISettingsTab
{
if (combo.Success)
{
- // Erste Option: Default (löscht Icon, lässt Mapping greifen).
+ // First option clears the icon and lets the default mapping take over.
if (
ImGui.Selectable(
HellionStrings.Tabs_Icon_DefaultOption,
@@ -148,7 +148,7 @@ internal sealed class Tabs : ISettingsTab
ImGui.Separator();
- // Pool-Optionen aus TabIconGlyphResolver.PickerOptions (Single-Source-of-Truth).
+ // Options sourced from TabIconGlyphResolver.PickerOptions (single source of truth).
foreach (var option in TabIconGlyphResolver.PickerOptions)
{
var isSelected = string.Equals(
@@ -305,10 +305,8 @@ internal sealed class Tabs : ISettingsTab
ImGui.SameLine();
- // Guard against an empty worlds list — can happen briefly
- // when switching characters or if the datacenter sheet
- // has not yet populated. Without the guard the indexed
- // access into worlds[selectedWorld] would crash.
+ // Guard against an empty worlds list (character switch or sheet not yet populated)
+ // to avoid an out-of-bounds crash on worlds[selectedWorld].
if (worlds.Count == 0)
{
ImGui.TextDisabled("(no worlds available)");
diff --git a/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs b/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs
index 013e55f..a14cd0f 100644
--- a/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs
+++ b/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs
@@ -272,9 +272,8 @@ internal sealed class ThemeAndLayout : ISettingsTab
ImGui.Separator();
ImGui.Spacing();
- // Slider 50–100 % UX-Range; intern 0.5–1.0 als WindowOpacity-Float.
- // Untere Schwelle 50 % verhindert versehentliches Komplett-Wegblenden
- // des Chat-Hintergrunds (war v1.2.0 Bug bei WindowAlpha=0).
+ // Slider range 50-100% maps to 0.5-1.0 internally. Floor at 50% prevents
+ // accidentally hiding the chat background (v1.2.0 bug at WindowAlpha=0).
var opacityPercent = Mutable.WindowOpacity * 100f;
if (
ImGuiUtil.DragFloatVertical(
diff --git a/HellionChat/Ui/SettingsTabs/ThemeMockup.cs b/HellionChat/Ui/SettingsTabs/ThemeMockup.cs
index 846a655..f81798f 100644
--- a/HellionChat/Ui/SettingsTabs/ThemeMockup.cs
+++ b/HellionChat/Ui/SettingsTabs/ThemeMockup.cs
@@ -7,15 +7,14 @@ namespace HellionChat.Ui.SettingsTabs;
internal static class ThemeMockup
{
- // Zeichnet ein Mini-Chat-Window-Mockup mit den Theme-Werten direkt
- // ins WindowDrawList. Keine Texture, keine Allocation pro Frame —
- // alles via DrawList.AddRectFilled / AddText.
+ // Mini chat window mockup drawn directly into the WindowDrawList.
+ // No textures, no per-frame allocations — pure AddRectFilled/AddText.
public static void Draw(Vector2 origin, Vector2 size, Theme theme)
{
var draw = ImGui.GetWindowDrawList();
var c = theme.Colors;
- // Window-Bg
+ // Window background
draw.AddRectFilled(
origin,
origin + size,
@@ -23,7 +22,7 @@ internal static class ThemeMockup
theme.Layout.WindowRounding
);
- // Title-Bar
+ // Title bar
var titleHeight = 14f;
draw.AddRectFilled(
origin,
@@ -32,7 +31,7 @@ internal static class ThemeMockup
theme.Layout.WindowRounding
);
- // Tab-Bar — 3 Mini-Tabs
+ // Tab bar (3 tabs)
var tabY = origin.Y + titleHeight + 4f;
var tabHeight = 12f;
for (var i = 0; i < 3; i++)
@@ -46,7 +45,7 @@ internal static class ThemeMockup
theme.Layout.TabRounding
);
- if (i == 0) // Active-Pill
+ if (i == 0) // active pill
{
draw.AddRectFilled(
new Vector2(tabX, tabY + tabHeight - 2f),
@@ -56,7 +55,7 @@ internal static class ThemeMockup
}
}
- // Card-Row mit Mock-Sender + Text
+ // Message card row
var rowY = tabY + tabHeight + 6f;
var rowHeight = 18f;
draw.AddRectFilled(
@@ -66,7 +65,7 @@ internal static class ThemeMockup
2f
);
- // Akzent-Button rechts unten
+ // Accent button (bottom right)
var btnW = 28f;
var btnH = 10f;
var btnX = origin.X + size.X - btnW - 6f;
@@ -78,7 +77,7 @@ internal static class ThemeMockup
theme.Layout.FrameRounding
);
- // Border um das gesamte Mockup
+ // Mockup border
draw.AddRect(
origin,
origin + size,
diff --git a/HellionChat/Ui/SettingsTabs/Window.cs b/HellionChat/Ui/SettingsTabs/Window.cs
index 827e9e9..7f7a0f1 100644
--- a/HellionChat/Ui/SettingsTabs/Window.cs
+++ b/HellionChat/Ui/SettingsTabs/Window.cs
@@ -107,7 +107,7 @@ internal sealed class Window : ISettingsTab
1,
10
);
- // Untergrenze von 2 Sekunden gegen Selbst-Soft-Lock.
+ // Floor at 2 seconds to prevent self-soft-lock.
Mutable.InactivityHideTimeout = Math.Max(2, Mutable.InactivityHideTimeout);
using (ImRaii.Disabled(Mutable.HideInBattle))
@@ -177,7 +177,6 @@ internal sealed class Window : ISettingsTab
ImGui.Checkbox(Language.Options_CanMove_Name, ref Mutable.CanMove);
ImGui.Checkbox(Language.Options_CanResize_Name, ref Mutable.CanResize);
- // v0.6.0 — global master switch for the pop-out input bar.
ImGui.Checkbox(
HellionStrings.Settings_Window_PopOutInputEnabled_Name,
ref Mutable.PopOutInputEnabled
@@ -186,9 +185,7 @@ internal sealed class Window : ISettingsTab
ImGui.Spacing();
- // Manual escape hatch for off-screen windows. The plugin already
- // runs an automatic bounds check once per session, but a button
- // is the user-friendly fallback after a display layout change.
+ // Fallback for off-screen windows after a display layout change.
if (ImGui.Button(HellionStrings.Settings_Window_ResetPosition_Name))
Plugin.ChatLogWindow.RequestPositionReset = true;
ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_ResetPosition_Description);
diff --git a/HellionChat/Ui/StatusBar.cs b/HellionChat/Ui/StatusBar.cs
index 16ac727..71c8d62 100644
--- a/HellionChat/Ui/StatusBar.cs
+++ b/HellionChat/Ui/StatusBar.cs
@@ -9,32 +9,23 @@ using HellionChat.Util;
namespace HellionChat.Ui;
-///
-/// Bottom-Status-Bar (v1.2.0). Fix 22 px hoch, BorderTop als Trenner.
-/// Slots links → rechts: Channel-Indicator (Color-Dot + Channel-Name),
-/// Privacy-Badge (Lock-Icon + Privacy-Label), Counts (Tabs + Msgs),
-/// Tells (Auto-Tell-Counter, hidden bei 0), Version (rechtsbündig, muted).
-///
-/// Update-Frequenz: 1×/Sekunde. Format-Strings werden zwischen Updates
-/// gecached, damit kein Per-Frame-Format-Allocation entsteht.
-///
+// Bottom status bar, 22px tall. Slots left to right: channel indicator,
+// privacy badge, counts, tells (hidden at 0), version (right-aligned).
+// Updates at 1Hz; format strings are cached between updates.
internal sealed class StatusBar
{
public const float Height = 22f;
private const long UpdateIntervalMs = 1000;
- // Cache-State — initial outdated, damit der erste Frame frisch berechnet.
+ // Initially outdated so the first frame always computes fresh.
private long _lastUpdateMs = -UpdateIntervalMs;
private string _cachedCountsText = string.Empty;
private string _cachedTellsText = string.Empty;
- ///
- /// Reine String-Logik — testbar ohne ImGui-Init.
- ///
+ // Pure string logic, testable without ImGui init.
public static string FormatCounts(int tabs, int messages)
{
- // InvariantCulture: User-System-Locale darf das Format nicht
- // verändern (de_DE würde sonst "1,2k" statt "1.2k" liefern).
+ // InvariantCulture so locale doesn't affect the format (e.g. de_DE "1,2k").
var msgPart =
messages >= 1000
? string.Format(CultureInfo.InvariantCulture, "{0:0.0}k msg", messages / 1000.0)
@@ -43,10 +34,7 @@ internal sealed class StatusBar
return $"{tabsPart} · {msgPart}";
}
- ///
- /// Reine String-Logik — testbar ohne ImGui-Init.
- /// 0 Tells → Leerstring (Slot wird ausgeblendet).
- ///
+ // Pure string logic, testable without ImGui init. Returns empty string at 0 tells.
public static string FormatTells(int count)
{
if (count <= 0)
@@ -54,8 +42,7 @@ internal sealed class StatusBar
return $"{count} {(count == 1 ? "tell" : "tells")}";
}
- // Single-pass replacement for the LINQ Sum+Count pair in Draw. Pure
- // helper so a future LINQ regression gets pinned by xUnit.
+ // Single-pass replacement for a LINQ Sum+Count pair. Pure helper for unit testing.
internal static (int messages, int tells) AggregateForStatusBar(IList tabs)
{
int messages = 0,
@@ -69,10 +56,7 @@ internal sealed class StatusBar
return (messages, tells);
}
- ///
- /// Test-Hook: Cache-Logic ohne reale Time-Source verifizieren.
- /// Nicht für Production-Render.
- ///
+ // Test hook to verify cache logic without a real time source.
internal (string counts, string tells) SnapshotForTest(
long now,
int tabs,
@@ -93,24 +77,18 @@ internal sealed class StatusBar
_lastUpdateMs = now;
}
- ///
- /// Render-Pfad. Aufrufer pusht bereits den HellionStyle/Theme;
- /// wir lesen nur die aktiven Theme-Farben und zeichnen.
- ///
public void Draw(Plugin plugin)
{
var theme = plugin.ThemeRegistry.Active;
var now = Environment.TickCount64;
- // Outer gate keeps the foreach out of the hot path 99% of frames.
- // UpdateCacheIfDue runs the same check internally — idempotent.
if (now - _lastUpdateMs >= UpdateIntervalMs)
{
var (messages, tells) = AggregateForStatusBar(Plugin.Config.Tabs);
UpdateCacheIfDue(now, Plugin.Config.Tabs.Count, messages, tells);
}
- // BorderTop als Trenner — DrawList-Line, ImGui-Separator hat zu viel Padding.
+ // Border top via DrawList -- ImGui.Separator has too much padding.
var cursorY = ImGui.GetCursorScreenPos().Y;
var winLeft = ImGui.GetWindowPos().X;
var winRight = winLeft + ImGui.GetWindowSize().X;
@@ -123,9 +101,9 @@ internal sealed class StatusBar
1f
);
- ImGui.Dummy(new Vector2(0, 2)); // BorderTop-Spacing
+ ImGui.Dummy(new Vector2(0, 2));
- // Slot 1: Active-Channel-Indicator
+ // Slot 1: active channel indicator
var inputCh = plugin.CurrentTab?.CurrentChannel?.Channel ?? InputChannel.Invalid;
var hasChannel = inputCh != InputChannel.Invalid;
var chatType = inputCh.ToChatType();
@@ -137,7 +115,7 @@ internal sealed class StatusBar
ImGui.SameLine();
ImGui.TextUnformatted(channelName);
- // Slot 2: Privacy-Badge — abgeleitet aus PrivacyFilterEnabled.
+ // Slot 2: privacy badge
ImGui.SameLine();
DrawSeparator();
ImGui.SameLine();
@@ -151,13 +129,13 @@ internal sealed class StatusBar
: HellionStrings.StatusBar_Privacy_Open;
ImGui.TextUnformatted(privacyLabel);
- // Slot 3: Counts
+ // Slot 3: counts
ImGui.SameLine();
DrawSeparator();
ImGui.SameLine();
ImGui.TextUnformatted(_cachedCountsText);
- // Slot 4: Tells (nur wenn > 0)
+ // Slot 4: tells (hidden at 0)
if (!string.IsNullOrEmpty(_cachedTellsText))
{
ImGui.SameLine();
@@ -166,7 +144,7 @@ internal sealed class StatusBar
ImGui.TextUnformatted(_cachedTellsText);
}
- // Slot 5: Version (rechtsbündig, muted)
+ // Slot 5: version, right-aligned, muted
var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
var versionWidth = ImGui.CalcTextSize(versionText).X;
var contentRegionMax = ImGui.GetContentRegionMax().X;
diff --git a/HellionChat/Ui/TabIconGlyphResolver.cs b/HellionChat/Ui/TabIconGlyphResolver.cs
index f9435dc..848c4c1 100644
--- a/HellionChat/Ui/TabIconGlyphResolver.cs
+++ b/HellionChat/Ui/TabIconGlyphResolver.cs
@@ -1,22 +1,11 @@
namespace HellionChat.Ui;
-///
-/// Reine String-Resolver-Logik ohne Dalamud-Dependency. Bewusst in
-/// eigener Datei (Dependency-Boundary auf File-Level sichtbar), damit
-/// Tests (HellionChat.Tests, Microsoft.NET.Sdk ohne Dalamud-Reference)
-/// sie aufrufen können, ohne dass die JIT beim Methodenaufruf die
-/// Dalamud-Assembly laden muss.
-///
-/// Wird im Settings-UI (T7) für die Glyph-Picker-Combobox und im
-/// Render-Code indirekt über
-/// verwendet.
-///
+// Pure string resolver logic with no Dalamud dependency, kept in its own
+// file so tests (HellionChat.Tests, no Dalamud reference) can call it directly.
+// Used in the settings UI glyph picker and indirectly via TabIconMapping.Resolve.
internal static class TabIconGlyphResolver
{
- ///
- /// Picker-Options-Pool — Single Source of Truth für das Glyph-Set.
- /// Reihenfolge ist die UI-Reihenfolge im Settings-Tab Icon-Combobox.
- ///
+ // Single source of truth for the glyph set; order matches the settings combobox.
public static readonly IReadOnlyList PickerOptions =
[
"comment",
@@ -36,20 +25,13 @@ internal static class TabIconGlyphResolver
"fire",
];
- ///
- /// Glyph-Set, das überhaupt als Override akzeptiert wird. Aus
- /// abgeleitet — KnownGlyphs nie
- /// manuell pflegen.
- ///
+ // Derived from PickerOptions -- never maintain this manually.
private static readonly HashSet KnownGlyphs = new(
PickerOptions,
StringComparer.OrdinalIgnoreCase
);
- ///
- /// Tab-Name → Default-Glyph-Name. Tab.Name wird per Lokalisierung
- /// gesetzt; wir matchen daher gegen einen Pool aus DE/EN-Synonymen.
- ///
+ // Tab.Name is localised, so we match against a pool of DE/EN synonyms.
private static readonly Dictionary NameDefaults = new(
StringComparer.OrdinalIgnoreCase
)
@@ -69,18 +51,11 @@ internal static class TabIconGlyphResolver
["tell"] = "envelope",
};
- ///
- /// Test-Surface: Glyph-Name-Resolver ohne Dalamud-Dependency.
- /// Reihenfolge:
- /// 1. Tab.Icon-Override (falls gesetzt und nicht nur Whitespace):
- /// a) bekannter Glyph → diesen Glyph
- /// b) unbekannter Glyph → harter Fallback "hashtag" (User hat
- /// bewusst etwas gesetzt, also überstimmt das die Defaults)
- /// 2. Auto-Tell-Tab → falls
- /// übergeben, sonst "clock".
- /// 3. Tab-Name-Default (-Lookup)
- /// 4. Fallback "hashtag"
- ///
+ // Resolves the glyph name for a tab. Priority order:
+ // 1. Tab.Icon override (if set): known glyph -> use it, unknown -> "hashtag"
+ // 2. Auto-tell tab -> autoTellGlyph if provided, else "clock"
+ // 3. Name default lookup
+ // 4. Fallback "hashtag"
public static string ResolveGlyphName(Tab tab, string? autoTellGlyph = null)
{
if (!string.IsNullOrWhiteSpace(tab.Icon))
diff --git a/HellionChat/Ui/TabIconMapping.cs b/HellionChat/Ui/TabIconMapping.cs
index 0bc08e8..a801e40 100644
--- a/HellionChat/Ui/TabIconMapping.cs
+++ b/HellionChat/Ui/TabIconMapping.cs
@@ -2,31 +2,14 @@ using Dalamud.Interface;
namespace HellionChat.Ui;
-///
-/// Default-Icon-Mapping für Tabs. v1.2.0 Layout-Refresh nutzt das
-/// in Top-Tabs (Icon-Prefix) und Sidebar (Icon-only mit Tooltip).
-/// User können in Settings → Tabs per Tab.Icon-Override eigene
-/// FontAwesome-Glyphen setzen.
-///
-/// Diese Klasse ist Dalamud-abhängig (FontAwesomeIcon-Enum). Die
-/// reine String-Resolver-Logik liegt bewusst in
-/// (eigene Datei, ohne
-/// Dalamud-Imports), damit Tests sie ohne Dalamud-Reference aufrufen
-/// können.
-///
+// Default icon mapping for tabs, used in top-tabs (icon prefix) and sidebar (icon-only with tooltip).
+// Users can override per tab via Settings -> Tabs -> Tab.Icon.
+// Pure string resolver logic lives in TabIconGlyphResolver (no Dalamud dependency) for testability.
internal static class TabIconMapping
{
- ///
- /// FontAwesome-Glyph-Name → Icon-Enum-Lookup. Wird für die
- /// Production-Resolve-API benötigt.
- ///
- /// INVARIANTE: Jeder Key in muss auch in
- /// stehen. Wird
- /// ein Glyph zu PickerOptions hinzugefügt, aber nicht hier, fällt
- /// die Override-Auflösung still auf
- /// zurück (degraded, kein Crash). Build-Time-Enforcement ist nicht
- /// möglich, weil PickerOptions ohne Dalamud-Reference auskommt.
- ///
+ // Glyph name -> FontAwesomeIcon lookup for production resolve.
+ // Every key must also exist in TabIconGlyphResolver.PickerOptions.
+ // A missing key silently falls back to FontAwesomeIcon.Hashtag (degraded, no crash).
private static readonly Dictionary GlyphLookup = new(
StringComparer.OrdinalIgnoreCase
)
@@ -48,23 +31,13 @@ internal static class TabIconMapping
["fire"] = FontAwesomeIcon.Fire,
};
- ///
- /// Production-Surface: liefert das Icon für einen Tab. Wrapper um
- /// plus
- /// Enum-Lookup. Wird von Render-Code (T3, T5) verwendet.
- ///
+ // Resolves the icon for a tab. Auto-tell tabs get a per-partner hashed icon
+ // from the tell pool so parallel tells differ by glyph shape, not just colour.
public static FontAwesomeIcon Resolve(Tab tab)
{
- // v1.2.0 — Auto-Tell-Tabs bekommen ein per-Partner gehashtes
- // Icon aus dem Tell-Pool. Damit unterscheiden sich parallele
- // Tells nicht nur über die Color (For), sondern auch über die
- // Glyph-Form. Berechnung bleibt hier (Dalamud-bound), weil
- // TellTarget Dalamud-Imports hat.
string? autoTellGlyph = null;
if (tab.IsTempTab && tab.TellTarget != null && tab.TellTarget.IsSet())
- {
autoTellGlyph = TabTintCache.GetIcon(tab);
- }
var glyph = TabIconGlyphResolver.ResolveGlyphName(tab, autoTellGlyph);
return GlyphLookup.TryGetValue(glyph, out var icon) ? icon : FontAwesomeIcon.Hashtag;
diff --git a/HellionChat/_Helpers/CompactInputHistoryNavigator.cs b/HellionChat/_Helpers/CompactInputHistoryNavigator.cs
index 3b2f8cc..0310aa6 100644
--- a/HellionChat/_Helpers/CompactInputHistoryNavigator.cs
+++ b/HellionChat/_Helpers/CompactInputHistoryNavigator.cs
@@ -2,16 +2,17 @@ using System;
namespace HellionChat._Helpers;
-// Pure-helper mirror of the compact pop-out history-navigation cursor
-// math. The original CompactCallback was tangled with ImGuiInputTextCallbackData
-// (DeleteChars/InsertChars), which can't be exercised in xUnit. The
-// ImGui buffer mutation stays at the call site; only the deterministic
-// cursor-and-replacement decision lives here.
+// Extracted history-navigation cursor math from CompactCallback to allow unit
+// testing without ImGuiInputTextCallbackData (DeleteChars/InsertChars).
+// Buffer mutation stays at the call site; only the cursor/replacement decision lives here.
//
// Index semantics match InputHistoryService:
-// index 0 = oldest entry
-// index Count - 1 = newest entry
-// cursor == -1 = "not browsing history"
+// index 0 = oldest entry
+// index Count-1 = newest entry
+// cursor == -1 = not browsing history
+//
+// replacement == null: caller must NOT touch the buffer (cursor unchanged).
+// replacement != null: write it to the buffer (including "" to clear it).
//
// TEST-MIRROR: ../../../Hellion Build test/Ui/CompactInputHistoryNavigatorTests.cs
public static class CompactInputHistoryNavigator
@@ -22,9 +23,6 @@ public static class CompactInputHistoryNavigator
Down,
}
- // replacement == null means: caller must NOT touch the buffer. This
- // distinguishes "cursor unchanged, leave the user's typing alone"
- // from "cursor moved to an empty slot, clear the buffer".
public static (int cursor, string? replacement) Navigate(
Direction direction,
int currentCursor,
@@ -38,7 +36,6 @@ public static class CompactInputHistoryNavigator
ArgumentNullException.ThrowIfNull(push);
ArgumentNullException.ThrowIfNull(getByCursor);
- var prev = currentCursor;
var next = currentCursor;
switch (direction)
@@ -46,8 +43,7 @@ public static class CompactInputHistoryNavigator
case Direction.Up:
if (currentCursor == -1)
{
- // First Up press from a fresh buffer: stash whatever
- // the user typed so they can recover it after browsing.
+ // Stash current input so the user can recover it after browsing.
var offset = 0;
if (!string.IsNullOrWhiteSpace(currentBuffer))
{
@@ -57,10 +53,9 @@ public static class CompactInputHistoryNavigator
next = getCount() - 1 - offset;
}
else if (currentCursor > 0)
- {
next--;
- }
break;
+
case Direction.Down:
if (currentCursor != -1)
{
@@ -71,10 +66,9 @@ public static class CompactInputHistoryNavigator
break;
}
- if (prev == next)
+ if (next == currentCursor)
return (next, null);
- var replacement = getByCursor(next) ?? string.Empty;
- return (next, replacement);
+ return (next, getByCursor(next) ?? string.Empty);
}
}
diff --git a/HellionChat/_Helpers/CompactInputSubmitter.cs b/HellionChat/_Helpers/CompactInputSubmitter.cs
index 7f1202b..546a9ae 100644
--- a/HellionChat/_Helpers/CompactInputSubmitter.cs
+++ b/HellionChat/_Helpers/CompactInputSubmitter.cs
@@ -3,11 +3,8 @@ using HellionChat.Ui;
namespace HellionChat._Helpers;
-// Pure-helper mirror of the compact pop-out submit flow. ChatInputBar's
-// SubmitCompact used to inline this against a sealed ChatLogWindow, which
-// blocks Moq-based isolation. Lifting the deterministic part into a POCO
-// keeps the production call site a one-liner while letting xUnit assert
-// the buffer/cursor reset and the sender contract directly.
+// Extracted submit logic from ChatInputBar.SubmitCompact to allow unit testing
+// without a sealed ChatLogWindow dependency.
// TEST-MIRROR: ../../../Hellion Build test/Ui/CompactInputSubmitterTests.cs
public static class CompactInputSubmitter
{
diff --git a/docs/AI_DISCLOSURE.md b/docs/AI_DISCLOSURE.md
index 92051fd..ede1c17 100644
--- a/docs/AI_DISCLOSURE.md
+++ b/docs/AI_DISCLOSURE.md
@@ -56,15 +56,18 @@ Both are good projects. Use what fits you best.
## Tooling
-| Tool | Purpose |
-| ----------------------------------------------------- | ------------------------------------------------------------- |
-| [Claude](https://claude.ai) (Anthropic) | Pair-level AI assistance via Claude Code CLI |
-| [VS Code](https://code.visualstudio.com) + C# Dev Kit | Primary IDE |
-| Dedicated Windows 11 VM | Build and in-game test environment (Dalamud requires Windows) |
-| [dalamud.dev](https://dalamud.dev) | Dalamud API reference |
-| [Microsoft Learn](https://learn.microsoft.com) | .NET and C# documentation |
-| [Context7](https://context7.com) | Up-to-date library docs for Claude context |
-| [Stack Overflow](https://stackoverflow.com) | General C# and .NET problem-solving |
+| Tool | Purpose |
+| ----------------------------------------------------- | ------------------------------------------------------------------- |
+| [Claude](https://claude.ai) (Anthropic) | Pair-level AI assistance via Claude Code CLI |
+| [VS Code](https://code.visualstudio.com) + C# Dev Kit | Primary IDE |
+| Dedicated Windows 11 VM | Build and in-game test environment (Dalamud requires Windows) |
+| [dalamud.dev](https://dalamud.dev) | Dalamud API reference |
+| [Microsoft Learn](https://learn.microsoft.com) | .NET and C# documentation |
+| [Context7](https://context7.com) | Up-to-date library docs for Claude context |
+| [Stack Overflow](https://stackoverflow.com) | General C# and .NET problem-solving |
+| Custom build test suite | Pattern-based integration tests written from scratch, drawing on |
+| | conventions from Lightless, Umbra and other standard FFXIV plugins. |
+| | Not publicly available. Yet. |
## Contact
diff --git a/docs/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md
index ab22fe8..daea5a3 100644
--- a/docs/CONTRIBUTORS.md
+++ b/docs/CONTRIBUTORS.md
@@ -1,87 +1,83 @@
# Contributors — Hellion Chat
-Hellion Chat ist von der Code-Seite ein Ein-Personen-Projekt. Aber ohne die Leute auf dieser Seite gäbe es weder die
-Bug-Fixes noch die UX-Verbesserungen, die seit den frühen Versionen reingelaufen sind. Jeder Eintrag hier hat das Plugin
-konkret besser gemacht.
+Hellion Chat is a one-person project on the code side. But without the people on this page, the bug fixes and UX
+improvements that have landed since the early versions would not exist. Every entry here has made the plugin concretely
+better.
-Die Anerkennung an die Upstream-Autoren von Chat 2 (Infi und Anna) liegt bewusst in [`../NOTICE.md`](../NOTICE.md),
-nicht hier. Diese Datei deckt explizit Beiträge zur Hellion-Chat-Seite ab.
+Attribution for the upstream Chat 2 authors (Infi and Anna) is intentionally in [`../NOTICE.md`](../NOTICE.md), not
+here. This file covers contributions to the Hellion Chat side specifically.
---
-## Entwicklung
+## Development
### JonKazama (Florian Wathling) — Maintainer
-Hellion Chat ist mein erstes FFXIV-Plugin und mein erstes größeres C#-/Dalamud-Projekt. Mein beruflicher Hintergrund ist
-Webentwicklung (Next.js, React, TypeScript, Prisma). Plugin-Entwicklung in einer fremden Codebase, ImGui,
-FFXIV-Game-Hooks und der gesamte Dalamud-Stack waren Neuland.
+Hellion Chat is my first FFXIV plugin and my first larger C#/Dalamud project. My professional background is web
+development (Next.js, React, TypeScript, Prisma). Plugin development in an unfamiliar codebase, ImGui, FFXIV game hooks
+and the entire Dalamud stack were new territory.
-Privacy-First-Defaults, Per-Channel-Retention, Auto-Tell-Tabs, Pop-Out-Input, ChatColours-Presets, Hellion-Theme plus
-Exo-2-Font und der v1.0.0-Standalone-Cut sind die Hellion-spezifischen Surface-Areas, die ich auf das Chat-2-Fundament
-aufgebaut habe. Die Lern-Geschichte dahinter steht in [`LEARNING-JOURNEY.md`](LEARNING-JOURNEY.md).
+Privacy-first defaults, per-channel retention, Auto-Tell-Tabs, pop-out input, ChatColours presets, the Hellion theme
+plus Exo 2 font, and the v1.0.0 standalone cut are the Hellion-specific surface areas I built on top of the Chat 2
+foundation. The learning story behind that is in [`LEARNING-JOURNEY.md`](LEARNING-JOURNEY.md).
-Hellion Chat ist Teil von [Hellion Online Media](https://hellion-media.de).
+Hellion Chat is part of [Hellion Online Media](https://hellion-media.de).
---
-## Tester
+## Testers
-Eine kurze Notiz vorneweg: Ich teste das Plugin nicht allein. Die Leute hier haben mir Bugs gemeldet, bevor sie bei mehr
-Nutzern aufgeschlagen wären. Sie haben UX-Probleme angesprochen, die ich blind nicht mehr gesehen habe. Und sie haben
-Feature-Wünsche eingebracht, die das Plugin in Richtungen geschoben haben, in die ich von alleine nicht gegangen wäre.
-Das ist nicht selbstverständlich. Externe Tester sind ihre Zeit wert.
+A quick note: I do not test this plugin alone. The people listed here reported bugs before they hit more users, raised
+UX problems I had gone blind to, and brought in feature requests that pushed the plugin in directions I would not have
+gone on my own. That is not a given. External testers are worth their time.
-### Carl Beleandis (Carla) — Beta-Tester
+### Carl Beleandis (Carla) — Beta Tester
-Carl testet seit der Bootstrap-Phase und hat sowohl die Pop-Out-Mechanik als auch die Theme-Richtung geprägt. Sein
-Feedback kommt direkt und ohne Umschweife und das ist genau, was ich beim Testen brauche.
+Carl has been testing since the bootstrap phase and has shaped both the pop-out mechanics and the theme direction.
+Feedback comes direct and without detours, which is exactly what I need when testing.
-Konkrete Beiträge:
+Concrete contributions:
-- **Pop-Out-Discoverability** — der Hinweis, dass Pop-Outs nur per Rechtsklick erreichbar waren, hat den Header-Button
- und den einmaligen Hint-Banner in v0.6.1 ausgelöst. Ich kannte den Rechtsklick-Pfad blind, deshalb hatte ich nicht
- mehr gesehen, dass neue Nutzer die Funktion gar nicht finden.
-- **/tell-Pop-Out-Mode** — der Wunsch, /tell-Tabs direkt als Pop-Out zu öffnen statt über den Tab-Umweg, ist in v0.6.1
- als opt-in Settings-Toggle gelandet. Bonus: Bei der Implementation ist ein alter Ghost-Window-Bug aufgefallen
- (LRU-Drop ließ Pop-Out-Fenster als Geister stehen), der gleich mit gefixt wurde.
-- **Theme-Varianten mit Helligkeits-Abstufungen** — der Wunsch nach einer Grün-Familie hat mein Verständnis von "ein
- Theme = eine Farbe" auf "Theme-Familien mit Stimmungs-Varianten" verschoben. Steht in der [Roadmap](ROADMAP.md) für
- einen späteren Cycle.
+- **Pop-out discoverability** — pointing out that pop-outs were only reachable via right-click triggered the header
+ button and the one-time hint banner in v0.6.1. I knew the right-click path by heart and had stopped seeing that new
+ users could not find the feature at all.
+- **/tell pop-out mode** — the request to open /tell tabs directly as a pop-out instead of going through the tab sidebar
+ landed in v0.6.1 as an opt-in settings toggle. Bonus: during implementation an old ghost-window bug surfaced (LRU drop
+ left pop-out windows as ghosts), which got fixed at the same time.
+- **Theme variants with brightness gradations** — the request for a green family shifted my thinking from "one theme =
+ one colour" to "theme families with mood variants". On the [roadmap](ROADMAP.md) for a later cycle.
-### Jin (Jingliu) — Alpha-Tester
+### Jin (Jingliu) — Alpha Tester
-Jin ist der aktive Tester der ersten Stunde und hat den Pop-Out-Workflow architektonisch in eine andere Richtung
-geschoben.
+Jin is the active tester from day one and pushed the pop-out workflow architecture in a different direction.
-Konkrete Beiträge:
+Concrete contributions:
-- **Pop-Out-Tab mit Input-Feld** — der Vorschlag, in einem Pop-Out auch tippen zu können (statt nur lesen), hat die
- v0.6.0 Pop-Out-Input-Bar ausgelöst. Das war ein größerer Refactor: Der Input-Layer aus `ChatLogWindow` musste so
- geöffnet werden, dass er auch in `Popout.cs` lebt, mit unabhängigem Text-Buffer und History-Cursor pro Pop-Out. Hat
- den Cycle dominiert, weil das Design erst sauber sein musste, bevor Code passieren konnte.
-- **TempTell Persistence** — der Wunsch, /tell-Tabs per Pin-Toggle einen Relog überleben zu lassen, steht in der
- [Roadmap](ROADMAP.md) für einen späteren Cycle. Berührt das Tab-System architektonisch und braucht eigenes Design.
+- **Pop-out tab with input bar** — the suggestion to be able to type in a pop-out (instead of just reading) triggered
+ the v0.6.0 pop-out input bar. That was a larger refactor: the input layer from `ChatLogWindow` had to be opened up so
+ it could also live in `Popout.cs`, with an independent text buffer and history cursor per pop-out. It dominated the
+ cycle because the design had to be clean before any code could happen.
+- **TempTell persistence** — the request for /tell tabs to survive a relog via a pin toggle is on the
+ [roadmap](ROADMAP.md) for a later cycle. It touches the tab system architecturally and needs its own design work.
---
-## Übersetzungen
+## Translations
-Hellion-eigene UI-Strings werden in `HellionChat/Resources/HellionStrings..resx` gepflegt.
+Hellion-specific UI strings are maintained in `HellionChat/Resources/HellionStrings..resx`.
-- **Deutsch (DE):** JonKazama (Native Speaker, Hauptsprache des Projekts)
+- **German (DE):** JonKazama (native speaker, primary project language)
-Die Upstream-Sprach-Dateien (`Language..resx`) sind nicht Teil dieser Datei. Sie werden über das
-[Chat-2-Crowdin-Projekt](https://github.com/Infiziert90/ChatTwo) gepflegt; Crowdin-Übersetzer findest du in den
-Plugin-Settings unter **Info → "Chat 2 community translators"**.
+Upstream language files (`Language..resx`) are not covered here. They are maintained via the
+[Chat 2 Crowdin project](https://github.com/Infiziert90/ChatTwo); Crowdin translators are listed in the plugin settings
+under **Info → "Chat 2 community translators"**.
---
-## Wie du beitragen kannst
+## How to Contribute
-Bug-Reports, Feature-Wünsche und Pull-Requests laufen über
-[Gitea Issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues). Workflow und Erwartungen stehen
-in [`../CONTRIBUTING.md`](../CONTRIBUTING.md), Code of Conduct in [`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md).
+Bug reports, feature requests and feedback are welcome — the best place to reach me is the Hellion Forge Discord:
+[discord.gg/X9V7Kcv5gR](https://discord.gg/X9V7Kcv5gR). Join and ping me in the Hellion Chat channel.
-Tester-Pool für neue Versionen läuft über den Hellion-Forge-Discord:
-[discord.gg/X9V7Kcv5gR](https://discord.gg/X9V7Kcv5gR). Wer in den Tester-Channel rein will, einfach im Forge melden.
+For pull requests and contribution guidelines see [`../CONTRIBUTING.md`](../CONTRIBUTING.md), Code of Conduct in
+[`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md).
diff --git a/docs/LEARNING-JOURNEY.md b/docs/LEARNING-JOURNEY.md
index 9b88d74..ada7b53 100644
--- a/docs/LEARNING-JOURNEY.md
+++ b/docs/LEARNING-JOURNEY.md
@@ -1,336 +1,331 @@
-# Entwicklungsgeschichte und Lernprozess
+# Development History and Learning Process
-## Hintergrund
+## Background
-Ich bin Autodidakt. Hellion Chat ist mein erstes FFXIV-Plugin und mein erstes größeres C#-Projekt. Mein beruflicher
-Hintergrund ist Webentwicklung (Next.js, React, TypeScript, Prisma, MySQL), also Browser-Welt mit JavaScript-Toolchain.
-C# kannte ich vor diesem Projekt nur oberflächlich, ImGui gar nicht, Dalamud nur als Endnutzer über andere Plugins.
+I am self-taught. Hellion Chat is my first FFXIV plugin and my first larger C# project. My professional background is
+web development (Next.js, React, TypeScript, Prisma, MySQL) — browser world with a JavaScript toolchain. I knew C# only
+superficially before this project, ImGui not at all, and Dalamud only as an end user through other plugins.
-Wenn ich an einer Stelle nicht weiterkomme, nutze ich AI-Tools wie Claude Code als Pair-Hilfsmittel. Wie das genau
-aussieht und welche Klassifikation ich verwende, steht transparent in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md).
+When I get stuck somewhere, I use AI tools like Claude Code as a pair assistant. What that looks like exactly and which
+classification I use is documented transparently in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md).
---
-## Warum überhaupt ein Chat-Plugin?
+## Why a chat plugin at all?
-Hellion Chat soll Chat 2 nicht ersetzen. Chat 2 liefert ein vollständiges Chat-Erlebnis mit kompletter Historie,
-Filtern, Suche und Replay. Für die meisten Nutzer ist genau das richtig.
+Hellion Chat is not meant to replace Chat 2. Chat 2 delivers a complete chat experience with full history, filters,
+search and replay. For most users that is exactly the right thing.
-### Zwei Millionen Nachrichten in zwei Jahren
+### Two million messages in two years
-Mein Wunsch nach einem engeren Default war ehrlich gesagt erstmal persönlich. Nach zwei Jahren mit Chat 2 lag meine
-Datenbank bei über zwei Millionen Nachrichten, der Großteil davon /say, /shout und /yell von wildfremden Leuten in
-Limsa. Genau diese Daten machen Chat 2's Voll-Historie nützlich, und die meisten Nutzer behalten sie auch gerne. Mein
-eigener Geschmack wollte einen kleineren Default. Also habe ich diesen Fork gebaut.
+My desire for a tighter default was honestly personal at first. After two years with Chat 2 my database had grown to
+over two million messages, the majority of them /say, /shout and /yell from complete strangers in Limsa. That is exactly
+what makes Chat 2's full history useful, and most users are happy to keep it. My own preference wanted a smaller
+default. So I built this fork.
-### Greeter in mehreren Clubs
+### Greeter in several clubs
-Dazu kam ein zweiter Use-Case: Ich bin in mehreren FFXIV-Clubs als Greeter aktiv. Für die Greeter-Arbeit reicht die
-Vanilla-Chat-Oberfläche nicht. Parallel laufende /tell-Gespräche schreiben in einem einzigen Tab durcheinander, und ich
-verliere ständig den Faden, wer mir gerade was geschrieben hat. Auto-Tell-Tabs (eines der frühen Hellion-Chat-Features)
-ist genau für diesen Workflow entstanden: ein Tab pro Gesprächspartner, automatisch gespawnt, mit manuellem
-Greeted-Status. Dass das auch der Privacy-Hygiene gut tut, war ein netter Bonus, nicht der Auslöser.
+There was a second use case: I am active as a greeter in several FFXIV clubs. The vanilla chat interface is not enough
+for greeter work. Parallel /tell conversations write into a single tab at the same time, and I constantly lose track of
+who wrote what. Auto-Tell-Tabs (one of the early Hellion Chat features) came directly from this workflow: one tab per
+conversation partner, automatically spawned, with a manual greeted status. The privacy hygiene benefit was a nice bonus,
+not the trigger.
### Hellion Online Media
-Die Privacy-Defaults sind außerdem eine Position aus meinem Hauptberuf. Hellion Online Media ist mein Einzelunternehmen,
-und Datenschutz gegenüber Kunden ist da kein Marketing-Slogan, sondern operativ relevant. Dieser Fork ist die
-Plugin-Form derselben Haltung.
+The privacy defaults also reflect a position from my main work. Hellion Online Media is my sole proprietorship, and data
+protection toward clients is not a marketing slogan there but operationally relevant. This fork is the plugin form of
+the same stance.
---
-## Warum nicht beim Original mitarbeiten?
+## Why not contribute to the original?
-Drei Gründe, in absteigender Wichtigkeit.
+Three reasons, in descending order of importance.
-### Defaults sind nicht verhandelbar, auch nicht meine
+### Defaults are not negotiable, including mine
-Privacy-First als Standard ist eine Minderheits-Position. Chat 2 bedient zu Recht die breite Masse mit Voll-Historie als
-Default. Diese Defaults im Upstream zu ändern wäre falsch gewesen. Ich hätte den Standard für eine große Nutzerbasis
-umgekippt, die ihn so wollte, wie er ist. Saubere Trennung über einen eigenen Plugin-Slot war der respektvollere Weg.
+Privacy-first as a default is a minority position. Chat 2 rightly serves the broad majority with full history as the
+default. Changing those defaults upstream would have been wrong. I would have flipped the standard for a large user base
+that wanted it as it was. A clean separation through a dedicated plugin slot was the more respectful path.
-### Das Webinterface musste weg
+### The web interface had to go
-Das ist ein zentrales Chat-2-Feature für Remote-Zugriff vom Zweitgerät. Ein PR der das entfernt, hat in einem gepflegten
-Upstream-Projekt keine Chance, und das ist auch richtig so. Aber genau das Webinterface kollidiert mit der
-Privacy-First-These dieses Forks: Ein Chat-Plugin das einen lokalen HTTP-Server startet, ist für mein Threat-Model eine
-zu große Angriffsfläche. Also raus damit.
+It is a central Chat 2 feature for remote access from a second device. A PR removing it has no chance in a
+well-maintained upstream project, and that is correct. But exactly that web interface conflicts with the privacy-first
+premise of this fork: a chat plugin that starts a local HTTP server is too large an attack surface for my threat model.
+So out it went.
-### Tempo
+### Velocity
-Ein Solo-Maintainer-Projekt mit kleinem Tester-Pool kann schneller iterieren als ein etabliertes Plugin mit großer
-Nutzerbasis. Das ist kein Vorwurf an Upstream, sondern eine andere Optimierung. Ich brauche keine Roadmap-Abstimmung,
-keine Reviewer-Verfügbarkeit, und kann Audit-Konsequenzen wie das Webinterface-Removal in einer einzigen Version
-durchziehen statt über mehrere Releases.
+A solo-maintainer project with a small tester pool can iterate faster than an established plugin with a large user base.
+That is not a criticism of upstream but a different optimization. I do not need roadmap alignment, reviewer
+availability, or to spread audit consequences like the web interface removal across multiple releases.
-EUPL-1.2 erlaubt das alles ausdrücklich, mit klarer Attribution. Der Code liegt offen unter derselben Lizenz wie Chat 2.
-Infi, Anna oder sonst jemand dürfen reinschauen, Ideen mitnehmen, Fragen stellen oder den Fork einfach ignorieren. Alles
-drei ist für mich okay.
+EUPL-1.2 explicitly allows all of this with clear attribution. The code is open under the same license as Chat 2. Infi,
+Anna, or anyone else can look in, take ideas, ask questions, or simply ignore the fork. All three are fine with me.
---
-## Wie ich so schnell release
+## How I release this fast
-Wer auf den Repo schaut, sieht in kurzer Zeit viele Releases und sehr viele Commits. Beides wird von außen gerne als
-Red-Flag gelesen: KI-Slop, Salami-Taktik, Code-Spam. Bei Hellion Chat ist beides eine bewusste Entscheidung, und ich
-erkläre lieber einmal warum, als mich später dafür zu rechtfertigen.
+Anyone looking at the repo sees a lot of releases and a high commit count in a short time. Both tend to read as red
+flags from the outside: AI slop, salami tactics, code spam. In Hellion Chat both are deliberate decisions, and I would
+rather explain them once than justify them later.
-### Vorarbeit, lange bevor der Fork existierte
+### Groundwork, long before the fork existed
-Bevor ich die erste Zeile in `HellionChat/` getippt habe, war ich wochenlang nur Leser. Chat 2 ingame nutzen und damit
-rumspielen. Issues im Upstream-Tracker durchgehen, vor allem die geschlossenen, weil dort steht, wie Infi und Anna Bugs
-einkreisen. Commits lesen, gerne auch ältere, um zu verstehen, warum eine Architektur-Entscheidung getroffen wurde,
-nicht nur, dass sie getroffen wurde. Wenn ich heute weiß, wo im Code was liegt, dann nicht, weil ich besonders schnell
-durch eine Codebase navigiere, sondern weil ich den Code vorher gelesen habe.
+Before I typed the first line into `HellionChat/`, I spent weeks as a reader. Using Chat 2 in-game and playing around
+with it. Going through issues in the upstream tracker, especially the closed ones, because that is where you see how
+Infi and Anna narrow down bugs. Reading commits, including older ones, to understand _why_ an architecture decision was
+made, not just _that_ it was made. If I know today where things live in the codebase, it is not because I navigate
+codebases particularly fast but because I read the code beforehand.
-Klingt nach Selbstverständlichkeit, ist es aber nicht. Die übliche Reihenfolge bei Solo-Forks heißt erst forken, dann
-verstehen. Ich habe es andersrum gemacht.
+That sounds obvious. It is not. The usual order for solo forks is fork first, understand later. I did it the other way
+around.
-### Die Codebase von Infi und Anna
+One thing I noticed reading the codebase closely: some patterns felt familiar in ways I had not expected, structural
+choices and comment styles that show up across a lot of modern plugin and tooling code regardless of how it was written.
+Nothing worth reading into. Coding workflows have changed a lot in the last few years across the board, and the traces
+of that show up everywhere. It did make me less self-conscious about my own workflow.
-Hellion Chat baut auf einem Boden auf, der schon flach ist. Chat 2 ist sauber strukturiert, die Naming-Konventionen sind
-konsistent, die Trennung zwischen Layern (Storage, UI, Game-Hooks, IPC) ist klar gezogen. Das ist in
-Open-Source-Plugin-Welten nicht selbstverständlich, und es ist der Hauptgrund, warum sich Hellion-spezifische Features
-oft "fast nativ" einbauen lassen. Ich muss nicht erst Spaghetti entwirren bevor ich was Eigenes danebenstellen kann.
+### Infi and Anna's codebase
-Side-Fact: Selbst beim ersten Codebase-Walkthrough mit Claude kam mehrfach der Hinweis, dass die Architektur
-ungewöhnlich gut aufgeräumt ist und mehrere Erweiterungspunkte vorbereitet. Das hat Gewicht, weil es von außen kommt,
-aber den eigentlichen Kredit kriegen Infi und Anna, nicht Claude.
+Hellion Chat builds on a foundation that is already flat. Chat 2 is cleanly structured, naming conventions are
+consistent, and the separation between layers (storage, UI, game hooks, IPC) is clearly drawn. That is not a given in
+open-source plugin land, and it is the main reason Hellion-specific features often slot in "almost natively". I do not
+have to untangle spaghetti before I can put something of my own next to it.
-### Atomar arbeiten, kleine Commits
+Side note: even during the first codebase walkthrough with Claude, the comment came up several times that the
+architecture is unusually tidy and has several extension points prepared. That carries weight because it comes from
+outside, but the actual credit goes to Infi and Anna, not Claude.
-Ein Commit, eine logische Änderung. Wenn ich einen Bug fixe, parallel eine Variable umbenenne und nebenbei einen
-Kommentar einbaue, sind das drei Commits, nicht einer. Klingt nach Mikro-Management, ist es aber nicht. Wenn in sechs
-Monaten ein Bug auftaucht und ich `git bisect` brauche, finde ich die kaputte Änderung in zwei Minuten statt in zwei
-Stunden. Bei einem 4000-Zeilen-Mega-Commit darf ich raten, welche der hundert Änderungen die kaputte ist.
+### Atomic work, small commits
-Den Stil habe ich bewusst auch deshalb beibehalten, weil Infi im Upstream häufig genauso arbeitet. Manchmal ein
-Sechs-Zeilen-Commit, manchmal nur ein Typo-Fix. Das ist keine Schwäche, das ist eine Entscheidung für lesbare
-Git-History. Den Stil im Fork beizubehalten ist ein Respekt-Move: Wer die beiden Repos vergleicht, soll den gleichen
-Lese-Rhythmus haben.
+One commit, one logical change. If I fix a bug, rename a variable and add a comment at the same time, that is three
+commits, not one. Sounds like micro-management, it is not. If a bug surfaces in six months and I need `git bisect`, I
+find the broken change in two minutes instead of two hours. With a 4000-line mega-commit I get to guess which of the
+hundred changes is the broken one.
-Bonus für mich persönlich: Kleine Commits zwingen mich, jeden Schritt einzeln zu durchdenken und zu benennen. Wenn ich
-nicht in zwei Sätzen erklären kann, was ein Commit macht, ist die Änderung wahrscheinlich noch nicht klar genug. Auf
-Beginner-Niveau ist das ein eingebauter Sanity-Check, den ich bei einem Big-Bang-Commit nicht hätte.
+I kept this style deliberately also because Infi works the same way upstream. Sometimes a six-line commit, sometimes
+just a typo fix. That is not a weakness, it is a decision for readable Git history. Keeping the style in the fork is a
+respect move: anyone comparing both repos should have the same reading rhythm.
-### AI als Beschleuniger, ehrlich
+Personal bonus: small commits force me to think through and name each step individually. If I cannot explain what a
+commit does in two sentences, the change is probably not clear enough yet. At beginner level that is a built-in sanity
+check I would not have with a big-bang commit.
-Ja, AI hilft beim Tempo, und nicht zu knapp. Ohne CodeRabbit hätte ich Critical-Bugs der Klasse
-`Equals/GetHashCode`-Anti-Pattern, Hook-Subscription-Leaks und TOCTOU-Races nicht gefunden. Ich bin schlicht zu
-unerfahren für diese Klasse von Findings, das schreibe ich genau so hin.
+### AI as an accelerator, honestly
-Was ich aber nicht mache: blind Code übernehmen, weil ein Tool ihn als Fix markiert hat. Bei mehreren
-CodeRabbit-Findings stand in den Original-Commits von Infi oder Anna sogar ein Stackoverflow-Link mit Begründung dabei,
-warum eine bestimmte Stelle so aussieht wie sie aussieht. Die habe ich gelesen, bevor ich was geändert habe. Erst
-verstehen, dann anfassen, dann committen. Das ist der Unterschied zwischen "AI gibt mir Code, ich pushe" und "AI zeigt
-mir wo's klemmt, ich entscheide".
+Yes, AI helps with velocity, and not a little. Without CodeRabbit I would not have found critical bugs like
+`Equals/GetHashCode` anti-patterns, hook subscription leaks and TOCTOU races. I am simply too inexperienced for that
+class of findings, and I write that exactly as it is.
-Klassifikation und konkrete Beispiele zur AI-Nutzung stehen in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). Hier in dieser
-Sektion ging es nur um den Tempo-Aspekt: Recherche plus saubere Codebase plus atomare Commits plus AI-gestütztes
-Review-Sparring sind die vier Faktoren zusammen. Kein einzelner davon erklärt das Tempo allein.
+What I do not do: blindly take code because a tool marked it as a fix. On several CodeRabbit findings, the original
+commits from Infi or Anna even included a Stack Overflow link explaining why a particular spot looks the way it does. I
+read those before touching anything. Understand first, then change, then commit. That is the difference between "AI
+gives me code, I push" and "AI shows me where it breaks, I decide".
+
+Classification and concrete examples of AI usage are in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). This section was only
+about the velocity aspect: research plus a clean codebase plus atomic commits plus AI-assisted review sparring are the
+four factors together. No single one explains the pace on its own.
---
-## Vom Web-Stack zu C# / Dalamud
+## From the web stack to C# / Dalamud
-### Type-System? Weniger Schock als erwartet
+### Type system? Less of a shock than expected
-C# nach TypeScript war angenehmer als gedacht. Properties statt getter/setter sind sauber, nullable reference types
-fühlen sich an wie `strict: true` in TypeScript. Ungewohnt war Wert-Typen vs. Referenz-Typen explizit denken zu müssen
-(`struct` vs. `class` mit echten Verhaltens-Konsequenzen), und Generics mit Constraints sind syntaktisch anders genug,
-dass ich beim Lesen kurz stocke. `async`/`await` ist semantisch ähnlich, aber Threading-Modelle sind in C# expliziter:
-`Task.Run`, `ConfigureAwait`, Synchronization-Contexts. Das hat mich mehrere Bugs gekostet, bevor ich verstanden hatte,
-wann der Main-Thread (in Plugin-Welt: der Framework-Tick) wirklich kritisch ist.
+C# after TypeScript was more comfortable than expected. Properties instead of getters/setters are clean, nullable
+reference types feel like `strict: true` in TypeScript. What was unfamiliar was having to think explicitly about value
+types versus reference types (`struct` vs. `class` with real behavioural consequences), and generics with constraints
+are syntactically different enough that I stumble on them while reading. `async`/`await` is semantically similar, but
+threading models are more explicit in C#: `Task.Run`, `ConfigureAwait`, synchronization contexts. That cost me several
+bugs before I understood when the main thread (in plugin land: the framework tick) is actually critical.
-### Build-Toolchain: ähnlich, aber anders
+### Build toolchain: similar, but different
-`dotnet` CLI, csproj-XML, NuGet sind funktional nicht weit weg von npm und tsconfig. Aber das XML-Format der csproj ist
-eine andere Sprache als JSON-Configs. Die Lock-Datei (`packages.lock.json`) musste ich erst aktiv aktivieren
-(`RestorePackagesWithLockFile=true`); das ist nicht Default. Im Web-Stack ist Lock-File-First Standard, im .NET-Stack
-offenbar nicht. Das war eine echte Überraschung.
+`dotnet` CLI, csproj XML, NuGet are functionally not far from npm and tsconfig. But the XML format of csproj is a
+different language than JSON configs. The lock file (`packages.lock.json`) had to be actively enabled
+(`RestorePackagesWithLockFile=true`); that is not the default. In the web stack, lock-file-first is standard, in the
+.NET stack apparently not. That was a real surprise.
-### ImGui ist eine andere Welt
+### ImGui is a different world
-Immediate-Mode-Rendering hat mit React-Component-Trees nichts gemein. Es gibt keine virtuelle DOM, keine Reconciliation,
-keinen "State der Komponente". Pro Frame zeichnet der Code die UI komplett neu, und der State lebt entweder in lokalen
-Variablen, die ich selbst verwalten muss, oder in der ImGui-eigenen ID-Stack-Logik.
+Immediate-mode rendering has nothing in common with React component trees. There is no virtual DOM, no reconciliation,
+no "component state". Every frame the code redraws the UI from scratch, and state lives either in local variables I
+manage myself or in ImGui's own ID stack logic.
-Was in React zwei Zeilen `useState` sind, ist in ImGui ein Member-Field plus manuelle ID-Stempel auf den Widgets, sonst
-kollidieren zwei Selectables in derselben Loop, weil sie auf die gleiche ID zurückfallen. Die ID-Stack-Kollision in
-`SearchSelector` (gefixt in v1.0.0) war genau dieses Symptom: Alle Selectables fielen auf dieselbe ambiguous ID zurück,
-bis ich den Row-Index in den Push-ID gemixt habe. Klassischer "warum klickt der falsche Eintrag"-Bug, den man nur
-findet, wenn man verstanden hat, wie ImGui IDs intern handhabt.
+What is two lines of `useState` in React is a member field plus manual ID stamps on widgets in ImGui, otherwise two
+selectables in the same loop collide because they fall back to the same ID. The ID stack collision in `SearchSelector`
+(fixed in v1.0.0) was exactly that symptom: all selectables fell back to the same ambiguous ID until I mixed the row
+index into the PushID. Classic "why is the wrong entry getting clicked" bug that you only find once you understand how
+ImGui handles IDs internally.
-### Dalamud-Spezifika
+### Dalamud specifics
-Plugin-Lifecycle, IPC-Subscriber-Pattern, Hook-System für Game-Functions, Game-Object-Threading. Viel davon war nur
-durch Lesen der Upstream-Codebase und durch [dalamud.dev](https://dalamud.dev) zu verstehen. Meine Trainings- und
-Such-Ergebnisse für "Dalamud" liefern oft veraltete API-Beispiele aus alten Versionen. dalamud.dev ist die zuverlässige
-Quelle. Wenn jemand neu anfängt: dort hin, nicht zu Stack Overflow.
+Plugin lifecycle, IPC subscriber pattern, hook system for game functions, game object threading. Much of that was only
+understandable through reading the upstream codebase and through [dalamud.dev](https://dalamud.dev). Search results for
+"Dalamud" often turn up outdated API examples from old versions. dalamud.dev is the reliable source. If someone is just
+starting out: go there, not to Stack Overflow.
-### Der Tag, an dem mich der DalamudPackager einen Tag gekostet hat
+### The day DalamudPackager cost me a day
-Dalamud SDK 15 liefert seinen eigenen Default-Packager mit, der Icons und Image-URLs ins Manifest einträgt. Ich hatte
-aus dem Upstream-Repo eine eigene `DalamudPackager.targets`-Datei mit `HandleImages`-Override übernommen, und die hat
-den SDK-Default überschrieben. Resultat: Das Manifest hatte keinen `IconUrl` mehr, und das Plugin tauchte in der
-Plugin-Liste ohne Icon auf.
+Dalamud SDK 15 ships its own default packager that writes icons and image URLs into the manifest. I had carried over a
+`DalamudPackager.targets` file from the upstream repo with a `HandleImages` override, and it was overriding the SDK
+default. Result: the manifest had no `IconUrl` anymore, and the plugin appeared in the plugin list without an icon.
-Symptom war einfach zu sehen, Ursache hat einen Tag gekostet. Ich hatte die Override-Datei für eine Pflicht-Datei
-gehalten, war sie aber nicht. Removal in v0.5.2, seitdem läuft der SDK-Default. Lektion: Erstmal mit Defaults arbeiten,
-Overrides erst wenn der Default nachweislich nicht passt.
+The symptom was easy to spot, the cause cost a day. I had treated the override file as mandatory when it was not.
+Removed in v0.5.2, SDK default running since then. Lesson: start with defaults, add overrides only when the default
+demonstrably does not fit.
---
-## Was ich aus dem Fork gelernt habe
+## What I learned from the fork
-### Refactor in einer fremden Codebase
+### Refactoring in an unfamiliar codebase
-Der Standalone-Cut in v1.0.0 hat die `ChatTwo.*`-Identität komplett auf `HellionChat.*` migriert. Klingt nach
-Find-and-Replace. War es nicht.
+The standalone cut in v1.0.0 migrated the entire `ChatTwo.*` identity to `HellionChat.*`. That sounds like find and
+replace. It was not.
-Konkret bedeutete das: Code-Namespace über alle 80 Source-Files plus 100 using-Direktiven plus zwei FQN-Aliases plus die
-Resource-Designer-Strings. Sechs IPC-Channels umbenannt (Breaking Change für Drittplugins, keine bekannten Anbindungen).
-Repo-Ordner-Struktur (`ChatTwo/` → `HellionChat/`) inklusive csproj, sln, allen GitHub-Workflows und der dependabot.yml.
-Public-Facing-Branding in README, repo.json, yaml auf Standalone-Framing umformuliert.
+In concrete terms: code namespace across all 80 source files plus 100 using directives plus two FQN aliases plus the
+resource designer strings. Six IPC channels renamed (breaking change for third-party plugins, no known integrations).
+Repo folder structure (`ChatTwo/` -> `HellionChat/`) including csproj, sln, all GitHub workflows and dependabot.yml.
+Public-facing branding in README, repo.json and yaml reformulated to standalone framing.
-Das war kein Solo-Find-and-Replace, weil Unicode-String-Pfade in Workflow-YAMLs anders quotiert werden müssen als
-C#-Strings. Weil Resource-Designer-Files generierte Inhalte haben, die nicht jede Toolchain im Blick hat. Und weil die
-`ChatTwo.*`-IPC-Channel-Namen Strings in `GetIpcSubscriber`-Calls sind: kein Symbol, kein Compile-Error, wenn man einen
-vergisst. Da merkst du, was alles still bleibt.
+It was not a solo find-and-replace because Unicode string paths in workflow YAMLs need different quoting than C#
+strings. Because resource designer files have generated content that not every toolchain tracks. And because the
+`ChatTwo.*` IPC channel names are strings in `GetIpcSubscriber` calls: no symbol, no compile error if you miss one. That
+is when you find out what stays quiet.
-### Sicherheit ist kein abstraktes Thema mehr
+### Security is no longer abstract
-Vor diesem Projekt war Supply-Chain-Sicherheit für mich akademisch. Drei konkrete Lektionen haben das geändert.
+Before this project, supply chain security was academic for me. Three concrete lessons changed that.
-**SQLite-Native-Binary.** Ich musste auf 3.50.3 pinnen (`SQLitePCLRaw.lib.e_sqlite3` Override), weil
-`Microsoft.Data.Sqlite` die transitiv nachgezogene Lib in einer Version mitschleppte, die CVE-2025-6965
-(Memory-Corruption durch Aggregate-Term-Overflow) und CVE-2025-7709 enthielt. Der Managed-Wrapper war neu, die
-Native-Lib war es nicht. Lektion: Transitive Dependencies prüfen sich nicht von selbst, du musst hinschauen.
+**SQLite native binary.** I had to pin to 3.50.3 (`SQLitePCLRaw.lib.e_sqlite3` override) because `Microsoft.Data.Sqlite`
+was pulling in a transitively referenced library at a version containing CVE-2025-6965 (memory corruption via aggregate
+term overflow) and CVE-2025-7709. The managed wrapper was new; the native library was not. Lesson: transitive
+dependencies do not audit themselves, you have to look.
-**Lock-File-Drift.** `packages.lock.json` honored bei `dotnet restore` (per `RestorePackagesWithLockFile=true` in der
-csproj) verhindert, dass transitive Versionen zwischen meiner Maschine und CI silent driften. Erst nach einem
-Build-Output-Mismatch zwischen lokal und GitHub-Actions hatte ich überhaupt verstanden, warum das nicht der Default ist.
+**Lock file drift.** `packages.lock.json` honoured via `RestorePackagesWithLockFile=true` in the csproj prevents
+transitive versions from silently drifting between my machine and CI. I only understood why this is not the default
+after a build output mismatch between local and GitHub Actions.
-**WrapText und der CodeQL-Alarm der drei Releases gekostet hat.** CodeQL hat in `ImGuiUtil.WrapText` einen
-Critical-Alert wegen "unvalidated local pointer arithmetic" geworfen. v0.5.2 hat einen Edge-Case validiert. Alert kam
-wieder. v0.5.3 hat den Buffer-Length via `GetByteCount` vor der Pointer-Math gecheckt. Alert kam wieder. v0.5.4 hat den
-ganzen Algorithmus auf `Span` und int-Offsets umgebaut, mit einem 16-KiB-Cap auf den ArrayPool-Rent. Erst da war Ruhe.
+**WrapText and the CodeQL alert that cost three releases.** CodeQL flagged a critical alert in `ImGuiUtil.WrapText` for
+unvalidated local pointer arithmetic. v0.5.2 validated an edge case. Alert came back. v0.5.3 checked buffer length via
+`GetByteCount` before the pointer math. Alert came back. v0.5.4 rebuilt the whole algorithm on `Span` and int offsets
+with a 16 KiB cap on the ArrayPool rent. Only then did it go quiet.
-Lektion: Wenn ein statischer Analyzer drei Mal hintereinander meckert, ist nicht der Analyzer überempfindlich. Die
-Datenflusslogik ist es.
+Lesson: when a static analyser complains three times in a row, the analyser is not oversensitive. The data flow logic
+is.
-### CodeRabbit als externer Code-Reviewer
+### CodeRabbit as an external code reviewer
-Der v1.0.0-Sweep hat 3 Critical und 21 Major Findings hochgespült. Drei Klassen davon waren besonders lehrreich:
+The v1.0.0 sweep surfaced 3 critical and 21 major findings. Three classes were particularly instructive:
-- **`Equals`-Methoden die `GetHashCode()` vergleichen.** Klassisches Hash-Kollisions-Anti-Pattern. Klingt nach "ist doch
- egal, wenn Hashes gleich sind, sind die Objekte auch gleich", ist aber genau falsch. Hashes können kollidieren,
- Objekte sind dann nicht gleich.
-- **`Dispose`-Methoden die nur einen Teil der Subscriptions wieder abmelden.** Leak bei jedem Plugin-Reload. Im
- Nutzer-Alltag merkst du das nicht sofort, im Long-Running-Test schon.
-- **TOCTOU-Races.** Zwischen Bounds-Check und Read kann ein anderer Thread das Array unter dir austauschen
+- **`Equals` methods comparing `GetHashCode()`.** Classic hash collision anti-pattern. Sounds like "if hashes are equal
+ the objects are equal", which is exactly backwards. Hashes can collide; the objects are not equal.
+- **`Dispose` methods that only unsubscribe part of their subscriptions.** Leak on every plugin reload. In normal use
+ you do not notice it immediately; in a long-running test you do.
+- **TOCTOU races.** Between a bounds check and a read another thread can swap out the array underneath you
(`GlobalParametersCache`, `AutoTranslate`).
-Davon hatte ich vorher bestenfalls die Theorie gelesen, nicht selbst diagnostiziert. CodeRabbit war für mich der Moment,
-wo "akademisches Wissen" zu "okay, das ist mein Code, das ist mein Bug" wurde.
+I had at best read the theory on all of these before, never diagnosed them in my own code. CodeRabbit was the moment
+where "academic knowledge" became "okay, that is my code, that is my bug".
-### Externe Tester sind ihr Gewicht in Gold wert
+### External testers are worth their weight
-Carlas Feedback zur Pop-Out-Discoverability hat den Header-Button in v0.6.1 ausgelöst. Dass Pop-Outs nur per Rechtsklick
-erreichbar waren, hatte ich als Maintainer nicht mehr gesehen, ich kannte den Pfad blind. Carls Wunsch nach
-Theme-Varianten mit Helligkeits-Abstufungen hat mein Verständnis von "ein Theme = eine Farbe" auf "Theme-Familien mit
-Stimmungs-Varianten" verschoben. Jingliu hat TempTell-Persistence gefordert, was das Tab-System architektonisch in Frage
-stellt.
+Carla's feedback on pop-out discoverability triggered the header button in v0.6.1. That pop-outs were only reachable via
+right-click was something I as maintainer had stopped seeing; I knew the path by heart. Carl's request for theme
+variants with brightness gradations shifted my thinking from "one theme = one colour" to "theme families with mood
+variants". Jingliu asked for TempTell persistence, which puts the tab system architecturally into question.
-Solo hätte ich diese drei Dinge nicht erkannt. Punkt.
+Solo I would not have seen any of those three things. Full stop.
-### release.yml und die Markdown-Hölle
+### release.yml and the YAML rabbit hole
-Der `release.yml`-Workflow ist beim ersten v0.6.0-Tag-Push einfach nicht losgegangen. Ich habe Stunden in Permissions,
-Secret-Scopes und Tag-Trigger-Konfiguration gegraben, bevor ich verstand, was eigentlich los war: Der
-PowerShell-Heredoc-Footer im "Generate release body"-Step enthielt eine `---`-Markdown-Horizontal-Rule an Spalte 1, und
-genau das hat das YAML-Block-Scalar von `run: |` beendet. GitHub konnte die Workflow-Datei nicht parsen, also hat der
-Push-Tag-Trigger nie registriert.
+The `release.yml` workflow simply did not fire on the first v0.6.0 tag push. I dug through permissions, secret scopes
+and tag trigger configuration for hours before I understood what was actually happening: the PowerShell heredoc footer
+in the "Generate release body" step contained a `---` Markdown horizontal rule at column 1, and that terminated the YAML
+block scalar of `run: |`. GitHub could not parse the workflow file, so the push-tag trigger never registered.
-Fix: Footer in eine externe `.github/release-footer.md` extrahiert, Workflow liest sie via `Get-Content` ein. Lektion:
-Wenn ein Workflow nicht triggert, verifiziere als Erstes, dass GitHub die Datei überhaupt parsen kann. Das war einer der
-Bugs, bei denen ich nach dem Fix kurz gelacht habe und mich dann gefragt, wie viele andere YAML-Dateien ich noch habe,
-die so eine Falle drin haben könnten.
+Fix: extracted the footer into an external `.github/release-footer.md`, workflow reads it via `Get-Content`. Lesson: if
+a workflow does not trigger, verify first that GitHub can even parse the file. That was one of the bugs where I laughed
+briefly after the fix and then asked myself how many other YAML files I had that might have the same trap in them.
---
-## Was ich noch lerne
+## What I am still learning
-### Performance-Profiling im Game-Context
+### Performance profiling in a game context
-Der FPS-Drop-Bug aus Upstream Chat 2 ([#145](https://github.com/Infiziert90/ChatTwo/issues/145)) ist auch in Hellion
-Chat noch nicht reproduziert oder verifiziert. v1.0.0 hat mehrere Fixes auf den verdächtigen Pfaden (DbViewer O(N²) →
-O(N), AutoTranslate Lock-Serialisierung, EmoteCache HttpClient-Reuse), aber das systematische Vermessen unter Last fehlt
-mir. Ich muss noch lernen, wie man im Plugin-Kontext sauber misst, was wirklich das Frame-Budget frisst.
+The FPS drop bug from upstream Chat 2 ([#145](https://github.com/Infiziert90/ChatTwo/issues/145)) has not been
+reproduced or verified in Hellion Chat. v1.0.0 applied several fixes on the suspected paths (DbViewer O(N²) -> O(N),
+AutoTranslate lock serialisation, EmoteCache HttpClient reuse), but systematic measurement under load is missing. I
+still need to learn how to properly measure what is actually consuming the frame budget in a plugin context.
-### Native-Interop und Pointer-Math
+### Native interop and pointer math
-Auch nach dem WrapText-Span-Refactor in v0.5.4 ist mir Pointer-Math unsicher. ImGui zwingt einen an mehreren Stellen in
-`unsafe`-Code, und der Sicherheitsabstand zur "unbounded ArrayPool allocation"-Klasse von Bugs ist schmaler als mir lieb
-ist. Da will ich besser werden, bevor ich tieferes ImGui-Custom-Drawing anfasse.
+Even after the WrapText Span refactor in v0.5.4, pointer math makes me uneasy. ImGui forces you into `unsafe` code in
+several places, and the safety margin from the "unbounded ArrayPool allocation" class of bugs is narrower than I would
+like. I want to get better at that before touching deeper ImGui custom drawing.
-### Test-Disziplin für Plugin-Code
+### Test discipline for plugin code
-Aktuell hat das Repo kein Test-Projekt. Das ist eine bewusste Entscheidung, keine vergessene. Plugin-Code mit
-FFXIV-Hooks und Dalamud-Lifecycle sauber zu testen ist nicht trivial, und ich hatte keinen Ansatz gefunden, der ohne
-riesiges Mocking-Gerüst sinnvoll wirkte. Privacy-Filter und Configuration-Migration wären gute Testkandidaten, weil sie
-isoliert sind. Steht auf der Liste, ist aber kein Quick-Win.
+The repo currently has no test project. That is a deliberate decision, not a forgotten one. Testing plugin code with
+FFXIV hooks and Dalamud lifecycle cleanly is non-trivial, and I had not found an approach that made sense without a
+large mocking scaffold. Privacy filter and configuration migration would be good test candidates because they are
+isolated. On the list, but not a quick win.
-### Linux-Eigenheiten unter Wine
+### Linux quirks under Wine
-XDG-Compliance, libnotify-Integration, WireGuard-Network-Detection, alles in der [Roadmap](ROADMAP.md), und alles
-technisch noch nicht ganz klar. Wine und sandboxed Plugin-Code teilen nicht alle System-APIs, und ich weiß nicht, wo die
-Stolperfallen liegen, bevor ich sie gefunden habe.
+XDG compliance, libnotify integration, WireGuard network detection, all on the [roadmap](ROADMAP.md), and all
+technically still unclear. Wine and sandboxed plugin code do not share all system APIs, and I do not know where the
+pitfalls are until I have found them.
---
-## Einsatz von AI-Tools
+## Use of AI tools
-Ich verwende Claude Code als Hilfsmittel, nicht als Ersatz für eigene Arbeit.
+I use Claude Code as an assistant, not as a replacement for my own work.
-**Wofür ich AI einsetze:**
+**What I use AI for:**
-- Debugging von Problemen, bei denen ich nach längerer Eigenrecherche nicht weiterkomme
-- Mustererkennen über große Codebasen hinweg (z. B. der ChatTwo→HellionChat-Sweep über 80 Dateien)
-- Verständnisfragen zu C#- und Dalamud-Konzepten, die mir noch nicht geläufig sind
-- Code-Review-Sparring, bevor ich CodeRabbit drauflasse
+- Debugging problems where I am stuck after extended research of my own
+- Pattern recognition across large codebases (e.g. the ChatTwo -> HellionChat sweep across 80 files)
+- Understanding questions on C# and Dalamud concepts I am not yet familiar with
+- Code review sparring before I run CodeRabbit on something
-**Was ich selbst mache:**
+**What I do myself:**
-- Architektur und Designentscheidungen
-- Privacy-First-Defaults und das Threat-Model dahinter
-- Tester-Kommunikation und Roadmap-Priorisierung
-- Reviewen, Verifizieren, Pushen
+- Architecture and design decisions
+- Privacy-first defaults and the threat model behind them
+- Tester communication and roadmap prioritisation
+- Reviewing, verifying, pushing
-Die Klassifikation und konkrete Beispiele stehen in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). Mir ist wichtig, dass Nutzer
-und potenzielle Beiträger verstehen, wie der Code zustande gekommen ist, gerade bei einem Plugin, das mit Nutzerdaten
-arbeitet.
+Classification and concrete examples are in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). It matters to me that users and
+potential contributors understand how the code came together, especially for a plugin that handles user data.
-Ja, AI. Ja, alleine. Beides öfter erwähnt als nötig. Willkommen im Open-Source-Plugin-Klima.
+Yes, AI. Yes, alone. Both mentioned more than strictly necessary. Welcome to the open-source plugin climate.
---
-## Warum diese Transparenz
+## Why this transparency
-Wer sich den Quellcode ansieht, soll wissen:
+Anyone reading the source code should know:
-- Ich bin kein professioneller C#- oder Plugin-Entwickler und lerne weiterhin dazu
-- AI-Unterstützung ist ein Werkzeug, kein Ghostwriter
-- Die Privacy-Position, die Designentscheidungen und die Roadmap sind meine
-- Ich versuche, meinen Code so sauber und sicher zu halten, wie meine aktuellen Fähigkeiten es zulassen
+- I am not a professional C# or plugin developer and am still learning
+- AI assistance is a tool, not a ghostwriter
+- The privacy position, the design decisions and the roadmap are mine
+- I try to keep my code as clean and secure as my current skills allow
-Hellion Chat ist auch ein Lernprojekt, und das soll man dem Repository ansehen dürfen.
+Hellion Chat is also a learning project, and that should be visible in the repository.
---
-## Verlinkungen
+## Links
-- [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) — KI-Pair-Disclosure mit Klassifikations-Schema
-- [`CONTRIBUTORS.md`](CONTRIBUTORS.md) — wer hat dieses Plugin neben mir besser gemacht
-- [`../NOTICE.md`](../NOTICE.md) — Anerkennung an Infi und Anna für das Chat-2-Fundament
-- [`ROADMAP.md`](ROADMAP.md) — geplante Cycles und Themen
+- [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) -- AI pair disclosure with classification schema
+- [`CONTRIBUTORS.md`](CONTRIBUTORS.md) -- who has made this plugin better alongside me
+- [`../NOTICE.md`](../NOTICE.md) -- attribution to Infi and Anna for the Chat 2 foundation
+- [`ROADMAP.md`](ROADMAP.md) -- planned cycles and topics
diff --git a/repo.json b/repo.json
index 9d8ba54..d616320 100644
--- a/repo.json
+++ b/repo.json
@@ -1,10 +1,10 @@
[
{
- "Author": "JonKazama-Hellion",
+ "Author": "Jon Kazama (Hellion Forge)",
"Name": "Hellion Chat",
"InternalName": "HellionChat",
"AssemblyVersion": "1.4.3.0",
- "Description": "Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV, forked from Chat 2 (EUPL-1.2). Tabs, channel filters, RGB colours, emotes, screenshot mode, and IPC integration all work the same. The optional web interface from Chat 2 is not included — it conflicts with the smaller default footprint this plugin is built around.\n\nOn top of the Chat 2 foundation, Hellion Chat adds data-handling controls that respect modern privacy rules across the EU, US and Japan. By default only your own conversations are stored; public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer.\n\nPrivacy features:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention periods with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (English and German) with live language switching\n- Own config file and database directory, no shared state with upstream Chat 2\n\nRecent releases:\nv1.3.0 — Honorific plugin integration: custom titles shown in the chat header, auto-detect with silent fallback.\nv1.4.0 — Lifecycle fixes: clean shutdown, explicit background threads, no lost config saves on disable.\nv1.4.1 — Theme engine performance pass (~13 % render-time recovery); Synthwave Sunset added as the tenth built-in theme.\nv1.4.2 — Chat log frame hot-path: three per-frame allocation patterns removed from the render loop.\nv1.4.3 — Async plugin load via IAsyncDalamudPlugin; schema gate replaces the migration chain for older configs.\n\nBased on Chat 2 by Infi and Anna, licensed under EUPL-1.2.\n\nSupport and community: Hellion Forge Discord at https://discord.gg/X9V7Kcv5gR",
+ "Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
"ApplicableVersion": "any",
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
"Tags": ["Social", "UI", "Chat", "Replacement", "Privacy"],
@@ -14,7 +14,7 @@
"CanUnloadAsync": false,
"LoadPriority": 0,
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
- "Changelog": "**Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover (2026-05-08)**\n\nPlugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin` API. The constructor now does only the bootstrap-essentials (config load, language init, conflict detection); migrations, service allocations, window construction and hook subscription move to LoadAsync. Dalamud can keep its UI responsive while the heavy work runs.\n\n- IAsyncDalamudPlugin two-phase load with per-line CaptureFailure in DisposeAsync (mirrors LightlessSync's pattern); idempotency guard protects against reload races\n- Schema-gate replaces the v9 → v16 migration chain. Configs on schema v16+ load directly; older configs trigger an \"install v1.4.2 first\" error so the historic migration path stays intact\n- AutoTranslate.PreloadCache moved off the load path. First use may have a sub-second hitch instead of every-load; the upstream chose differently, we accept first-use latency\n- FontManager.BuildFonts is called sync at the start of LoadAsync; Dalamud rebuilds the font atlas on its own pipeline so the custom Hellion-Exo2 font appears with a brief font-pop after load (matches ChatTwo's behaviour)\n- Custom-repo URL moved to gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat. GitHub repo stays as a frozen v1.4.2 snapshot; new releases ship from Gitea. Existing testers need to update the custom-repo URL once\n- Plugin-load time in this release sits at ~3.7 s median (5 reloads), comparable to v1.4.2. Async migration is foundational for v1.4.4 Lazy-Init optimisations rather than an immediate user-perceived win\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**\n\nThird sub-patch of the v1.4.x Polish Sweep series. Per-frame allocations from the chat-log render path eliminated.\n\n- DrawMessages card-mode hoists theme/drawList/winLeft/winRight/borderColorAbgr out of the per-message loop. About 500 redundant calls per frame at 100 visible messages, multiplied by every pop-out window\n- Auto-tell tab tint and icon use a per-tab cache. Hash computation and string allocation only happen when the tell target name or world drifts. AutoTellTabTint stays a pure hash helper; cache lives in a thin TabTintCache wrapper\n- Status bar gates its tab aggregation behind the same one-second cache it already used for the format strings. LINQ Sum and Count replaced with a single foreach pass that runs on roughly 1% of frames\n\nRealistic frame-time recovery: 2-5% in typical scenes, more on pop-out-heavy setups because the card-border hoist scales per window.\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.4.1 — Theme Engine Performance**\n\nSecond sub-patch of the v1.4.x Polish Sweep series. Heap pressure from the theme engine's per-frame render path removed, plus a tenth built-in theme and hardening for the custom-theme hot-reload.\n\n- Theme records carry a pre-computed ABGR-packed cache for every color slot; cache is filled when the theme is registered and refreshed defensively on every Switch()\n- HellionStyle.PushGlobal reads ABGR values from the cache instead of calling ColourUtil.RgbaToAbgr per slot per frame; ~13 % render-time recovery measured in typical scenes (plan estimate was 2–6 %, real ~10–15 %)\n- ThemeRegistry custom-theme reload distinguishes a recoverable file lock (editor mid-save) from a permanent IO failure; locked themes keep their last-known-good snapshot and retry on the next lookup instead of dropping out of the picker\n- New built-in: Synthwave Sunset — Hot Magenta + Cyan on midnight violet, 80s neon-grid vibes; tenth theme in the picker\n- Author credits refreshed: brand themes are credited as \"Hellion Forge\"; Mint Grove and Forge Merchantman now credited to Carla Beleandis as a community thanks\n\nNo schema bump, no user-visible behaviour change other than smoother frames on GC-sensitive setups and one additional colour option.\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.4.0 — Critical Lifecycle Fixes**\n\nFirst sub-patch of the v1.4.x Polish Sweep series. Seven known lifecycle and race bugs eliminated before any performance refactor sits on top.\n\n- MessageStore disposal no longer triggers GC.Collect globally; Pooling=false on the SQLite connection means there's nothing left to clean up by hand\n- PendingMessage and RetentionSweep worker threads are explicitly marked IsBackground=true so the plugin domain can unload during XIVLauncher reload without waiting for them\n- EmoteCache image and gif loaders moved from async-void to async Task with a shared task tracker, draining on Dispose so an in-flight load can no longer write to a disposed EmoteImages entry\n- DisposeAsync 10s timeout now warns loudly instead of silently leaving the worker behind\n- Plugin.Dispose flushes any pending DeferredSaveFrames before tearing services down, so settings changes made in the last few frames before disable are no longer lost\n- The v13→v14 config migration now reads the pre-v13 backup and carries HellionThemeWindowOpacity into the new WindowOpacity field instead of falling back to the default 0.85\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nEarlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
+ "Changelog": "**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy startup work (migrations, hooks, windows) now runs async so Dalamud's UI stays responsive during load. Load time is comparable to v1.4.2 — this is the foundation for v1.4.4 optimisations.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\n**v1.4.2 — Smoother frames in the chat log**\n\nPer-frame allocations in the chat-log render path eliminated. 2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.\n\n- Card-mode: theme/border invariants hoisted out of the per-message loop\n- Auto-tell tab tint and icon cached per tab\n- Status bar aggregation runs on ~1% of frames instead of every frame\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
"AcceptsFeedback": true,
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip",
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip",