chore: comments, second pass -- the task codes the first pass missed

The first sweep matched a character class that swallowed the digit, so a bare
B1 slipped through while B1-2 was caught. Searching the whole A-Z space instead
of guessing prefixes turned up 130-odd more: B0 through B6, C2, C3, D1, H2, M6,
P7, P8, T2, W2, plus GP-04, KB-01, OD-1, PM-1, PM-3, SEC-01, TR-4, TR-7, UI-11,
UI-12, XC-8 and API-3.

Kept deliberately: 41 B4 01 is a byte signature, "N0" a format string,
#L119-L128 a source anchor, LS4/LS6 are linkshells, and A=FF B=0C G=41 R=C2
explains a colour-channel order. Those look like codes and are not.

Also translated the eight German comments left in the theme files and
ImGuiUtil. Seven of them described what a palette does to which channel, which
is worth reading -- just not in a second language in an otherwise English
codebase.
This commit is contained in:
2026-08-19 22:03:12 +02:00
parent 5b738e6885
commit 16557213cd
59 changed files with 262 additions and 193 deletions
+4 -4
View File
@@ -23,7 +23,7 @@ internal sealed class AutoTellTabsService : IDisposable
private readonly ILogger<AutoTellTabsService> _logger; private readonly ILogger<AutoTellTabsService> _logger;
// Tabs-list structure lock now lives on Plugin (neutral owner) so the // Tabs-list structure lock now lives on Plugin (neutral owner) so the
// MessageManager refilter can share it. See Plugin.TabsListLock / B3. // MessageManager refilter can share it. See Plugin.TabsListLock.
private object TabsListLock => _plugin.TabsListLock; private object TabsListLock => _plugin.TabsListLock;
// Bumped whenever something wipes unpinned temp tabs wholesale (logout). // Bumped whenever something wipes unpinned temp tabs wholesale (logout).
@@ -179,7 +179,7 @@ internal sealed class AutoTellTabsService : IDisposable
} }
// Three steps, because building the tab pulls history out of the store and // Three steps, because building the tab pulls history out of the store and
// that must not happen under TabsListLock (B3 rule; the query sorts the whole // that must not happen under TabsListLock (the query sorts the whole
// receiver history). Step 1 and 3 are locked, step 2 is not. // receiver history). Step 1 and 3 are locked, step 2 is not.
int generation; int generation;
lock (TabsListLock) lock (TabsListLock)
@@ -303,7 +303,7 @@ internal sealed class AutoTellTabsService : IDisposable
internal void DropOldestTempTab() internal void DropOldestTempTab()
{ {
// B3: lock the list-structure ops so the (currently caller-less) Unpin path // Lock the list-structure ops so the (currently caller-less) Unpin path
// can't race the worker; re-entrant when HandleTell already holds the lock. // can't race the worker; re-entrant when HandleTell already holds the lock.
lock (TabsListLock) lock (TabsListLock)
{ {
@@ -577,7 +577,7 @@ internal sealed class AutoTellTabsService : IDisposable
// Count and flag under one lock so the cap can't be raced. SaveConfig stays // Count and flag under one lock so the cap can't be raced. SaveConfig stays
// OUTSIDE -- holding TabsListLock across a save would put an fsync on the // OUTSIDE -- holding TabsListLock across a save would put an fsync on the
// click path, which is what B6 just removed elsewhere. // click path, which is what a later cycle just removed elsewhere.
lock (TabsListLock) lock (TabsListLock)
{ {
if (PinnedTempTabCount >= MaxPinnedTempTabs) if (PinnedTempTabCount >= MaxPinnedTempTabs)
+3 -3
View File
@@ -1,7 +1,7 @@
namespace HellionChat; namespace HellionChat;
// Reduced CJK fallback coverage for the v1.5.3 NotoSansCjk fallback merge (B1). // Reduced CJK fallback coverage for the v1.5.3 NotoSansCjk fallback merge.
// Before B1 the fallback merged over the full `Ranges` array (Default + endonyms), // Before that the fallback merged over the full `Ranges` array (Default + endonyms),
// duplicating the Latin/Default work already done by the global/Japanese fonts. // duplicating the Latin/Default work already done by the global/Japanese fonts.
// This is the trimmed remainder the fallback is actually the sole source for: // This is the trimmed remainder the fallback is actually the sole source for:
// - Hangul Syllables (AC00-D7A3): no other merged font ships Korean glyphs. // - Hangul Syllables (AC00-D7A3): no other merged font ships Korean glyphs.
@@ -9,7 +9,7 @@ namespace HellionChat;
// font is Inter-Light (no CJK), so the fallback is the SOLE Han source. The JpRange // font is Inter-Light (no CJK), so the fallback is the SOLE Han source. The JpRange
// overlap is harmless (MergeMode: the Japanese font wins for shared kanji). // overlap is harmless (MergeMode: the Japanese font wins for shared kanji).
// Deliberately excluded: ONLY the ASCII/Latin Default block (0x20-0xFF), which the // Deliberately excluded: ONLY the ASCII/Latin Default block (0x20-0xFF), which the
// global font already owns -- that doubled Latin merge is the B1 waste being removed. // global font already owns -- that doubled Latin merge is the waste being removed.
// Kept as plain start/end pairs so it is unit-testable without the unsafe ImGui // Kept as plain start/end pairs so it is unit-testable without the unsafe ImGui
// glyph-range builder (mirrors FontSizeResolver's split-for-test rationale). // glyph-range builder (mirrors FontSizeResolver's split-for-test rationale).
internal static class CjkFallbackRange internal static class CjkFallbackRange
+5 -5
View File
@@ -35,7 +35,7 @@ public class ConfigKeyBind
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration
{ {
internal const int LatestVersion = 26; internal const int LatestVersion = 27;
public int Version { get; set; } = LatestVersion; public int Version { get; set; } = LatestVersion;
@@ -45,7 +45,7 @@ public class Configuration : IPluginConfiguration
// Global window opacity, applied across all themes. // Global window opacity, applied across all themes.
public float WindowOpacity = 0.85f; public float WindowOpacity = 0.85f;
// UI-12: background opacity of the main chat window while unfocused. // Background opacity of the main chat window while unfocused.
// WindowOpacity above stays the focused value. // WindowOpacity above stays the focused value.
public float WindowOpacityInactive = 0.65f; public float WindowOpacityInactive = 0.65f;
@@ -161,7 +161,7 @@ public class Configuration : IPluginConfiguration
public bool SeenPopOutHeaderHint; public bool SeenPopOutHeaderHint;
public bool AutoTellTabsOpenAsPopout; public bool AutoTellTabsOpenAsPopout;
// UI-7: how sender names are rendered in the chat log. // How sender names are rendered in the chat log.
public WorldSuffixMode WorldSuffixMode = WorldSuffixMode.OtherWorldOnly; public WorldSuffixMode WorldSuffixMode = WorldSuffixMode.OtherWorldOnly;
public NameFormMode NameFormMode = NameFormMode.Full; public NameFormMode NameFormMode = NameFormMode.Full;
@@ -232,7 +232,7 @@ public class Configuration : IPluginConfiguration
// Toast when a tell the user sent could not be delivered. // Toast when a tell the user sent could not be delivered.
public bool NotifyFailedTell = true; public bool NotifyFailedTell = true;
// UI-11: warn before sending a message that carries plugin-only glyphs. // Warn before sending a message that carries plugin-only glyphs.
public bool NotifyPluginDisclosure = true; public bool NotifyPluginDisclosure = true;
public bool KeepInputFocus = true; public bool KeepInputFocus = true;
public bool Use24HourClock = true; public bool Use24HourClock = true;
@@ -462,7 +462,7 @@ public class Tab
[NonSerialized] [NonSerialized]
internal string? _cachedTellIcon; internal string? _cachedTellIcon;
// PM-3 hover-lerp state. Default 0f means "not hovered". Sidebar // hover-lerp state. Default 0f means "not hovered". Sidebar
// path animates per tab; card-mode-border path is tab-aggregate // path animates per tab; card-mode-border path is tab-aggregate
// (any card-row hover ramps the alpha for all cards in this tab). // (any card-row hover ramps the alpha for all cards in this tab).
// Lerp speed lives in the render loop, not here, so the same field // Lerp speed lives in the render loop, not here, so the same field
+6 -6
View File
@@ -94,13 +94,13 @@ public sealed class FontManager : IDisposable
private ushort[] Ranges = []; private ushort[] Ranges = [];
private ushort[] JpRange = []; private ushort[] JpRange = [];
// B1: trimmed remainder the NotoSansCjk fallback is the sole source for // Trimmed remainder the NotoSansCjk fallback is the sole source for
// (Hangul + full Han); excludes the Default/Latin block already merged // (Hangul + full Han); excludes the Default/Latin block already merged
// by the global font, so the fallback no longer re-merges the full Ranges array. // by the global font, so the fallback no longer re-merges the full Ranges array.
private ushort[] CjkFallbackGlyphRange = []; private ushort[] CjkFallbackGlyphRange = [];
// Report accessor for the ctor self-test: built glyph-range array lengths so // Report accessor for the ctor self-test: built glyph-range array lengths so
// the step can show the B1 dedup effect (a small trimmed fallback vs the large // the step can show the dedup effect (a small trimmed fallback vs the large
// primary range) in its on-disk report instead of a bare Pass. // primary range) in its on-disk report instead of a bare Pass.
internal (int Ranges, int JpRange, int CjkFallback) GlyphRangeLengths => internal (int Ranges, int JpRange, int CjkFallback) GlyphRangeLengths =>
(Ranges.Length, JpRange.Length, CjkFallbackGlyphRange.Length); (Ranges.Length, JpRange.Length, CjkFallbackGlyphRange.Length);
@@ -247,7 +247,7 @@ public sealed class FontManager : IDisposable
// Instance method so Ranges / JpRange are reachable without parameter // Instance method so Ranges / JpRange are reachable without parameter
// plumbing; PascalCase field names follow the existing class style. // plumbing; PascalCase field names follow the existing class style.
// B1: shared CJK + symbols tail for both the regular and italic delegate // Shared CJK + symbols tail for both the regular and italic delegate
// fonts. Earlier-merged fonts win for shared codepoints (imgui MergeMode), // fonts. Earlier-merged fonts win for shared codepoints (imgui MergeMode),
// so this runs AFTER the primary font is set as config.MergeFont. The CJK // so this runs AFTER the primary font is set as config.MergeFont. The CJK
// fallback is the sole Hangul/Simplified-Han source when UseHellionFont=true // fallback is the sole Hangul/Simplified-Han source when UseHellionFont=true
@@ -265,7 +265,7 @@ public sealed class FontManager : IDisposable
config.GlyphRanges = JpRange; config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese"); AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
// NotoSansCjk fallback, trimmed to CjkFallbackGlyphRange (B1). Merged last so earlier fonts win. // NotoSansCjk fallback, trimmed to CjkFallbackGlyphRange. Merged last so earlier fonts win.
config.SizePt = basePt; config.SizePt = basePt;
config.GlyphRanges = CjkFallbackGlyphRange; config.GlyphRanges = CjkFallbackGlyphRange;
AddFontWithFallback( AddFontWithFallback(
@@ -427,7 +427,7 @@ public sealed class FontManager : IDisposable
// Common extras (Axis ingame glyphs, endonyms, enclosed alphanumerics) // Common extras (Axis ingame glyphs, endonyms, enclosed alphanumerics)
// belong to the primary/Japanese ranges only. The trimmed CJK fallback // belong to the primary/Japanese ranges only. The trimmed CJK fallback
// (B1) skips them so it stays a pure Hangul/Simplified-Han remainder and // skips them so it stays a pure Hangul/Simplified-Han remainder and
// does not re-merge the Default-block work the global font already did. // does not re-merge the Default-block work the global font already did.
if (includeCommonExtras) if (includeCommonExtras)
{ {
@@ -498,7 +498,7 @@ public sealed class FontManager : IDisposable
); );
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges, includeCommonExtras: true); JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges, includeCommonExtras: true);
// B1: the fallback gets only the trimmed Hangul/Simplified-Han remainder. // The fallback gets only the trimmed Hangul/Simplified-Han remainder.
// No Default block, no endonyms — those are already merged by the global and // No Default block, no endonyms — those are already merged by the global and
// Japanese fonts, so re-merging them on the fallback was wasted atlas work. // Japanese fonts, so re-merging them on the fallback was wasted atlas work.
CjkFallbackGlyphRange = BuildRange(CjkFallbackRange.Pairs, includeCommonExtras: false); CjkFallbackGlyphRange = BuildRange(CjkFallbackRange.Pairs, includeCommonExtras: false);
+7 -7
View File
@@ -507,14 +507,14 @@ internal unsafe class KeybindManager : IDisposable
// Resolve the surface this keybind acts on FIRST: a focused pop-out otherwise // Resolve the surface this keybind acts on FIRST: a focused pop-out otherwise
// the main window. Channel-set/REPLY/prefill all write here so the action // the main window. Channel-set/REPLY/prefill all write here so the action
// follows the input the user is typing in (C3 full tail rebuild, OD-1). // follows the input the user is typing in.
var (targetWindow, targetTab) = ResolveKeybindTarget(); var (targetWindow, targetTab) = ResolveKeybindTarget();
// Surface + focus the resolved target ONCE, before routing. Main: ActivateChat // Surface + focus the resolved target ONCE, before routing. Main: ActivateChat
// re-surfaces it from a hide/closed state (the chat-activation entry point // re-surfaces it from a hide/closed state (the chat-activation entry point
// retired in v1.6.0). Pop-out: arm only its focus — NOT ActivateChat, which // retired in v1.6.0). Pop-out: arm only its focus — NOT ActivateChat, which
// would yank the main window to front and un-hide it on every pop-out-targeted // would yank the main window to front and un-hide it on every pop-out-targeted
// keybind (OD-1: stay where the user types). Exactly one window arms focus per // keybind: stay where the user types. Exactly one window arms focus per
// keybind, so the next frame has no SetKeyboardFocusHere race. // keybind, so the next frame has no SetKeyboardFocusHere race.
if (targetWindow is ChannelPopoutWindow) if (targetWindow is ChannelPopoutWindow)
targetWindow.RequestInputFocus(); targetWindow.RequestInputFocus();
@@ -547,7 +547,7 @@ internal unsafe class KeybindManager : IDisposable
// Rotation binds (REPLY / linkshell-cycle). Ported from v1.5.6's // Rotation binds (REPLY / linkshell-cycle). Ported from v1.5.6's
// ChatLogWindow.Activated (1d3b429:240-334) without the ChatActivatedArgs // ChatLogWindow.Activated (1d3b429:240-334) without the ChatActivatedArgs
// indirection (gone in the rewrite). Writes onto the resolved surface's // indirection (gone in the rewrite). Writes onto the resolved surface's
// tab (C2/C3 shared target), not Plugin.CurrentTab. // tab, not Plugin.CurrentTab.
if (targetTab is { } rotTab) if (targetTab is { } rotTab)
{ {
var targetChannel = (InputChannel?)rotateChannel; var targetChannel = (InputChannel?)rotateChannel;
@@ -629,7 +629,7 @@ internal unsafe class KeybindManager : IDisposable
if (info.Permanent) if (info.Permanent)
{ {
// KB-01 (1.5.6 parity, ChatLogWindow.SetChannel 1d3b429:1476-1479): // 1.5.6 parity (ChatLogWindow.SetChannel, 1d3b429:1476-1479):
// committing the game channel also pre-targets the game's native input. // committing the game channel also pre-targets the game's native input.
// Forward the tab's reply target for Tell so the partner is armed // Forward the tab's reply target for Tell so the partner is armed
// game-side (ChangeChatChannel code 17); null for a linkshell — // game-side (ChangeChatChannel code 17); null for a linkshell —
@@ -654,7 +654,7 @@ internal unsafe class KeybindManager : IDisposable
// Prefill text binds (CMD_COMMAND seeds "/"): the token always goes to the // Prefill text binds (CMD_COMMAND seeds "/"): the token always goes to the
// main InputBar (the focus contract does not expose pop-out buffers); a // main InputBar (the focus contract does not expose pop-out buffers); a
// focused pop-out already received focus above, so only token routing matters // focused pop-out already received focus above, so only token routing matters
// here (documented scope limit, OD-1). // here -- a documented scope limit.
if (info.Text is { } text) if (info.Text is { } text)
Plugin.Instance.InputBar.SetPendingMessage(text); Plugin.Instance.InputBar.SetPendingMessage(text);
} }
@@ -665,7 +665,7 @@ internal unsafe class KeybindManager : IDisposable
} }
// Resolve which chat surface a keybind action targets: the open pop-out whose // Resolve which chat surface a keybind action targets: the open pop-out whose
// input currently has focus, otherwise the main window. C2/C3 share this so a // input currently has focus, otherwise the main window. Both paths share it so a
// channel-switch/REPLY/prefill follows the surface the user is typing in. The // channel-switch/REPLY/prefill follows the surface the user is typing in. The
// returned tab is that surface's bound tab (pop-out: Bound; main: ActiveTab). // returned tab is that surface's bound tab (pop-out: Bound; main: ActiveTab).
// Null tab => skip the tab-write (early-load window where no tab exists yet). // Null tab => skip the tab-write (early-load window where no tab exists yet).
@@ -682,7 +682,7 @@ internal unsafe class KeybindManager : IDisposable
} }
// Tab-delta keybinds (ChatTabForward/Backward) stay main-window-only by design: // Tab-delta keybinds (ChatTabForward/Backward) stay main-window-only by design:
// a channel-bound pop-out has no tab list to cycle (OD-1). The focus contract is // a channel-bound pop-out has no tab list to cycle. The focus contract is
// consumed by the channel-set/REPLY/prefill tail, not here. // consumed by the channel-set/REPLY/prefill tail, not here.
private void DispatchTabDelta(int delta) private void DispatchTabDelta(int delta)
{ {
+1 -1
View File
@@ -31,7 +31,7 @@ public sealed class ExtraChat : IDisposable
// volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these. // volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these.
// Reference assignment is atomic on x64, but the barrier ensures visibility // Reference assignment is atomic on x64, but the barrier ensures visibility
// across threads (especially Mono/Wine). See AUDIT-2026-05-05 [SEC-01]. // across threads (especially Mono/Wine). Raised in the 2026-05-05 audit.
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new(); private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => internal IReadOnlyDictionary<string, uint> ChannelCommandColours =>
ChannelCommandColoursInternal; ChannelCommandColoursInternal;
+3 -3
View File
@@ -163,7 +163,7 @@ internal class MessageManager : IAsyncDisposable
internal void ClearAllTabs() internal void ClearAllTabs()
{ {
// B3: snapshot the tab LIST under the shared lock so the worker-thread // Snapshot the tab LIST under the shared lock so the worker-thread
// add/remove can't tear the enumeration; tab.Clear() then runs lock-free // add/remove can't tear the enumeration; tab.Clear() then runs lock-free
// (each tab's Messages has its own SemaphoreSlim — lock order: list outer). // (each tab's Messages has its own SemaphoreSlim — lock order: list outer).
List<Tab> tabsSnapshot; List<Tab> tabsSnapshot;
@@ -184,8 +184,8 @@ internal class MessageManager : IAsyncDisposable
using var messages = Store.GetMostRecentMessages(CurrentContentId, since); using var messages = Store.GetMostRecentMessages(CurrentContentId, since);
// TempTabs excluded (live state from AutoTellTabsService). Bucket via the // TempTabs excluded (live state from AutoTellTabsService). Bucket via the
// pure MapMessagesToTabs so the assignment stays testable outside Dalamud (B3-1). // pure MapMessagesToTabs so the assignment stays testable outside Dalamud.
// B3: snapshot under the shared lock (list copy only — short critical // Snapshot under the shared lock (list copy only — short critical
// section). The Store query above and the AddSortPrune writes below stay // section). The Store query above and the AddSortPrune writes below stay
// OUTSIDE the lock (lock order: list outer, MessageList inner). // OUTSIDE the lock (lock order: list outer, MessageList inner).
List<Tab> nonTempTabs; List<Tab> nonTempTabs;
+1 -1
View File
@@ -2,7 +2,7 @@ using HellionChat.Resources;
namespace HellionChat; namespace HellionChat;
// UI-7: how a sender's name is rendered in the chat log. Kept in its own file // How a sender's name is rendered in the chat log. Kept in its own file
// (no Dalamud usings) so the SenderNameFormatter pure-helper test stays // (no Dalamud usings) so the SenderNameFormatter pure-helper test stays
// AppDomain-isolated (feedback_dalamud_test_isolation). // AppDomain-isolated (feedback_dalamud_test_isolation).
+1 -1
View File
@@ -264,7 +264,7 @@ internal sealed class PayloadHandler
if (!Sheets.IsInForay()) if (!Sheets.IsInForay())
{ {
// Single SetPendingMessage call; v1.5.6 used incremental Chat += writes. // Single SetPendingMessage call; v1.5.6 used incremental Chat += writes.
// XC-8: shares the /tell builder with the native detours. IsPublic (not // Shares the /tell builder with the native detours. IsPublic (not
// IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World. // IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World.
_inputBar.SetPendingMessage( _inputBar.SetPendingMessage(
GameFunctions.Chat.BuildTellCommand( GameFunctions.Chat.BuildTellCommand(
+80 -13
View File
@@ -134,7 +134,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal Integrations.HonorificService HonorificService { get; private set; } = null!; internal Integrations.HonorificService HonorificService { get; private set; } = null!;
internal Integrations.CustomAudioPlayer CustomAudioPlayer { get; private set; } = null!; internal Integrations.CustomAudioPlayer CustomAudioPlayer { get; private set; } = null!;
// Ctor-smoke anchors (B0-2). Exposed so the Payload/Chunk ctor-smoke steps // Ctor-smoke anchors. Exposed so the Payload/Chunk ctor-smoke steps
// can drive the real per-frame Lender path (Borrow()) and the eager // can drive the real per-frame Lender path (Borrow()) and the eager
// singletons through the container, never via new(). Mirror of the // singletons through the container, never via new(). Mirror of the
// FontManager property pattern — every SelfTest reaches services this way. // FontManager property pattern — every SelfTest reaches services this way.
@@ -178,7 +178,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal bool ChatActivationRequested; internal bool ChatActivationRequested;
// Set in the first DisposeAsync statement so async callbacks scheduled // Set in the first DisposeAsync statement so async callbacks scheduled
// via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail // via Framework.RunOnTick (v1.4.8 retention sweep) can early-bail
// before they touch state that has already been torn down. Volatile // before they touch state that has already been torn down. Volatile
// because the tick reads it from a different thread than the writer. // because the tick reads it from a different thread than the writer.
private volatile bool _isDisposing; private volatile bool _isDisposing;
@@ -188,7 +188,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// just unloaded belongs to nobody. // just unloaded belongs to nobody.
internal bool IsDisposing => _isDisposing; internal bool IsDisposing => _isDisposing;
// v1.9.0 B5: last full Draw() wall-time in ms, written once per frame at // v1.9.0: last full Draw() wall-time in ms, written once per frame at
// the end of the UiBuilder.Draw handler. Covers the GlobalStyleScope push // the end of the UiBuilder.Draw handler. Covers the GlobalStyleScope push
// and the font push (the first-frame hitch measurement must include atlas/style // and the font push (the first-frame hitch measurement must include atlas/style
// prologue cost), not just WindowSystem.Draw — measuring the inner call // prologue cost), not just WindowSystem.Draw — measuring the inner call
@@ -214,7 +214,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// must never block doing so. // must never block doing so.
internal readonly Util.DbOperationGate DbOperations = new(); internal readonly Util.DbOperationGate DbOperations = new();
// B3: neutral owner of the Config.Tabs LIST-structure lock so both the // Neutral owner of the Config.Tabs LIST-structure lock so both the
// worker-thread mutator (AutoTellTabsService) and the framework-thread // worker-thread mutator (AutoTellTabsService) and the framework-thread
// refilter (MessageManager) share ONE monitor. Lock order: this outer, // refilter (MessageManager) share ONE monitor. Lock order: this outer,
// MessageList's SemaphoreSlim inner — never the reverse. // MessageList's SemaphoreSlim inner — never the reverse.
@@ -287,6 +287,37 @@ public sealed class Plugin : IAsyncDalamudPlugin
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10." + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10."
); );
} }
// 2.0.0 does not migrate, it starts over. Five cycles rebuilt the whole
// window layer, and a config carried through them keeps values chosen
// against surfaces that no longer exist -- an opacity picked for a
// window that has been redrawn twice since, tabs laid out for a sidebar
// that works differently now. Every user of this build is a tester who
// was told this happens, and it is the only way to be sure everyone
// sees the same defaults.
//
// The file is copied aside first. Rolling back to 1.5.6 is a supported
// move here and it stays cheap: the old settings are a file copy away,
// rather than an evening of clicking them back in.
if (Config.Version < 27)
{
BackUpConfigBeforeReset();
Config = Configuration.CreateFresh();
Config.Version = 27;
// Saved immediately: a crash between here and the first user-driven
// save would otherwise run the whole reset again on the next start,
// and the second run would back up the already-reset file over the
// real backup.
SaveConfig();
Log.Information(
"Config reset to defaults for 2.0.0. Previous settings kept as "
+ "HellionChat.json.pre-2.0.0.bak next to the config file."
);
}
else
{
// v23 migration: SidebarTabView was the 1.5.6 sidebar↔top-tabs switch, // v23 migration: SidebarTabView was the 1.5.6 sidebar↔top-tabs switch,
// superseded by MainWindowLayoutMode in the v1.6.0 rewrite. A user who // superseded by MainWindowLayoutMode in the v1.6.0 rewrite. A user who
// set it false (only effective in 1.5.6) wanted top tabs — carry that // set it false (only effective in 1.5.6) wanted top tabs — carry that
@@ -341,7 +372,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
var carried = 0; var carried = 0;
foreach (var tab in Config.Tabs) foreach (var tab in Config.Tabs)
{ {
if (tab.NameCameFromPartner || (!tab.IsTempTab && tab.TellTarget?.IsSet() != true)) if (
tab.NameCameFromPartner
|| (!tab.IsTempTab && tab.TellTarget?.IsSet() != true)
)
continue; continue;
tab.NameCameFromPartner = true; tab.NameCameFromPartner = true;
@@ -356,14 +390,15 @@ public sealed class Plugin : IAsyncDalamudPlugin
); );
} }
} }
}
Config.Version = 26; Config.Version = 27;
// Unpinned TempTabs are session-only and dropped on every load. Pinned // Unpinned TempTabs are session-only and dropped on every load. Pinned
// TempTabs survive reload -- tester feedback in v1.4.7. // TempTabs survive reload -- tester feedback in v1.4.7.
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad); Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
// GP-04: clear stale Tab.PopOut flags now — the pool binds further down // Clear stale Tab.PopOut flags now — the pool binds further down
// (ChannelPopoutPool resolve below), so at this point no tab can own a // (ChannelPopoutPool resolve below), so at this point no tab can own a
// slot. A persisted PopOut=true (notably on surviving pinned TempTabs) // slot. A persisted PopOut=true (notably on surviving pinned TempTabs)
// would otherwise be a flag with no window. Runs after the strip, before // would otherwise be a flag with no window. Runs after the strip, before
@@ -462,7 +497,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>(); FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
ChannelPopoutPool = _host.Services.GetRequiredService<Ui.Windows.ChannelPopoutPool>(); ChannelPopoutPool = _host.Services.GetRequiredService<Ui.Windows.ChannelPopoutPool>();
// Ctor-smoke anchors (B0-2). Resolved last, against the fully built // Ctor-smoke anchors. Resolved last, against the fully built
// container: every MakePayloadHandler dep (MainWindow, InputBar, // container: every MakePayloadHandler dep (MainWindow, InputBar,
// ChunkRenderer, ...) is resolvable here, and the ChunkRenderer resolve // ChunkRenderer, ...) is resolvable here, and the ChunkRenderer resolve
// below just reuses the same cached singleton. These are plain // below just reuses the same cached singleton. These are plain
@@ -732,7 +767,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
return; return;
// Set before any cleanup so deferred Framework.RunOnTick callbacks // Set before any cleanup so deferred Framework.RunOnTick callbacks
// (B3 retention sweep) see the flag and bail out before they touch // (the retention sweep) see the flag and bail out before they touch
// MessageManager / Log / static fields that the rest of this method // MessageManager / Log / static fields that the rest of this method
// is about to tear down. // is about to tear down.
_isDisposing = true; _isDisposing = true;
@@ -840,6 +875,38 @@ public sealed class Plugin : IAsyncDalamudPlugin
return failure; return failure;
} }
// Copies the config aside before the 2.0.0 reset overwrites it. Best effort
// by design: a backup that cannot be written must not stop the plugin from
// starting, and the reset itself is what the user was told would happen.
//
// Overwrite=false, so a second run cannot bury the real backup under a copy
// of the already-reset file. The reset saves immediately for the same
// reason, but a crash in between is exactly when this matters.
private static void BackUpConfigBeforeReset()
{
try
{
var dir = Interface.ConfigDirectory.Parent?.FullName;
if (dir is null)
return;
var configFile = Path.Combine(dir, "HellionChat.json");
if (!File.Exists(configFile))
return;
var backup = Path.Combine(dir, "HellionChat.json.pre-2.0.0.bak");
if (File.Exists(backup))
return;
File.Copy(configFile, backup);
Log.Information($"HellionChat: config backed up to {backup} before the 2.0.0 reset");
}
catch (Exception e)
{
Log.Warning(e, "HellionChat: could not back up the config before the 2.0.0 reset");
}
}
private static void MigrateFromChatTwoLayout() private static void MigrateFromChatTwoLayout()
{ {
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
@@ -1181,7 +1248,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// Schedule on the next framework tick to avoid the ~194ms // Schedule on the next framework tick to avoid the ~194ms
// hitch from blocking with .Wait() while the frame finishes. // hitch from blocking with .Wait() while the frame finishes.
// The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is // The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is
// now guarded by the shared Plugin.TabsListLock (B3), so this // now guarded by the shared Plugin.TabsListLock, so this
// tick scheduling is purely hitch-avoidance, not safety. // tick scheduling is purely hitch-avoidance, not safety.
// Pattern reference: SimpleTweaks // Pattern reference: SimpleTweaks
// Tweaks/Chat/CaseInsensitiveCommands.cs:45. // Tweaks/Chat/CaseInsensitiveCommands.cs:45.
@@ -1268,7 +1335,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
private void Draw() private void Draw()
{ {
// v1.9.0 B5: time the whole handler (style + font prologue included). // v1.9.0: time the whole handler (style + font prologue included).
// Bail before measuring once teardown has begun — a late Draw tick // Bail before measuring once teardown has begun — a late Draw tick
// must not touch ThemeRegistry / FontManager after DisposeAsync. // must not touch ThemeRegistry / FontManager after DisposeAsync.
if (_isDisposing) if (_isDisposing)
@@ -1277,7 +1344,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
var drawWatch = Stopwatch.StartNew(); var drawWatch = Stopwatch.StartNew();
try try
{ {
// v1.4.8 B2: pick up external edits of the active custom theme JSON // v1.4.8: pick up external edits of the active custom theme JSON
// without forcing the user to re-click the picker. The disk-stat is // without forcing the user to re-click the picker. The disk-stat is
// 1Hz-throttled inside RefreshActiveIfStale, so this is essentially // 1Hz-throttled inside RefreshActiveIfStale, so this is essentially
// free on built-in themes and ~1 stat/second on custom themes. // free on built-in themes and ~1 stat/second on custom themes.
@@ -1367,7 +1434,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// Config.Tabs across the save so JSON includes them. Cloning only the // Config.Tabs across the save so JSON includes them. Cloning only the
// unpinned subset keeps the allocation proportional to // unpinned subset keeps the allocation proportional to
// AutoTellTabsLimit (<=15) instead of the full tab list. // AutoTellTabsLimit (<=15) instead of the full tab list.
// B3: the strip/restore mutates the tab LIST, so it shares TabsListLock // The strip/restore mutates the tab LIST, so it shares TabsListLock
// with the worker add/remove and the refilter snapshot. Re-entrant: the // with the worker add/remove and the refilter snapshot. Re-entrant: the
// one worker caller (HandleTell) already holds it; framework callers take // one worker caller (HandleTell) already holds it; framework callers take
// it here. SavePluginConfig runs inside (short, in-memory) — the documented fallback // it here. SavePluginConfig runs inside (short, in-memory) — the documented fallback
+1 -1
View File
@@ -5,7 +5,7 @@ using HellionChat.Util;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B2: behavioural check that the card path feeds CardClipPlanner AND that a // Behavioural check that the card path feeds CardClipPlanner AND that a
// layout change clears the height cache — not a non-null check. Pure plan math // layout change clears the height cache — not a non-null check. Pure plan math
// is pinned headless by CardClipPlanTests; this drives the live accessors. // is pinned headless by CardClipPlanTests; this drives the live accessors.
internal sealed class CardClipPlanStep : ISelfTestStep internal sealed class CardClipPlanStep : ISelfTestStep
@@ -15,7 +15,7 @@ namespace HellionChat.SelfTests;
// - a BOUND auto-tell tab keeps its partner by design (leg1) — its binding is // - a BOUND auto-tell tab keeps its partner by design (leg1) — its binding is
// Tab.TellTarget and is deliberately untouched by the strip. // Tab.TellTarget and is deliberately untouched by the strip.
// The channel label is intentionally NOT asserted: a tell tab re-derives back to // The channel label is intentionally NOT asserted: a tell tab re-derives back to
// Tell after the strip (spec TR-7); only the target matters for privacy. // Tell after the strip ; only the target matters for privacy.
internal sealed class CurrentTabGuidedStep : ISelfTestStep internal sealed class CurrentTabGuidedStep : ISelfTestStep
{ {
private readonly Plugin _plugin; private readonly Plugin _plugin;
+1 -1
View File
@@ -5,7 +5,7 @@ using HellionChat._Helpers;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B2-3: proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist) // Proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist)
// scanner into the REAL send entry InputBar.TrySend. Drives TrySend via the // scanner into the REAL send entry InputBar.TrySend. Drives TrySend via the
// arm-test-hook with a PUA glyph in the buffer and NotifyPluginDisclosure on: // arm-test-hook with a PUA glyph in the buffer and NotifyPluginDisclosure on:
// the first send must ARM and HOLD (no send), so PendingMessage stays the probe // the first send must ARM and HOLD (no send), so PendingMessage stays the probe
@@ -112,7 +112,7 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
return SelfTestStepResult.Fail; return SelfTestStepResult.Fail;
} }
// B1: assert the atlas actually finished building all required handles, // Assert the atlas actually finished building all required handles,
// not just that the references are non-null. FontsReady is the observable // not just that the references are non-null. FontsReady is the observable
// state the trimmed-fallback rebuild must still reach; a half-built atlas // state the trimmed-fallback rebuild must still reach; a half-built atlas
// would pass the null/exception checks above but fail here. // would pass the null/exception checks above but fail here.
@@ -128,7 +128,7 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
} }
// Report what was actually verified rather than a bare Pass. // Report what was actually verified rather than a bare Pass.
// The glyph-range entry counts make the B1 dedup visible — the cjk-fallback // The glyph-range entry counts make the dedup visible — the cjk-fallback
// range is now a small trimmed remainder next to the large primary range. // range is now a small trimmed remainder next to the large primary range.
var counts = fm.GlyphRangeLengths; var counts = fm.GlyphRangeLengths;
var italicState = var italicState =
@@ -4,7 +4,7 @@ using HellionChat.Ui.Windows;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// P8 wiring: UserHide() suppresses DrawConditions; both ActivateChat() (Enter) and // wiring: UserHide() suppresses DrawConditions; both ActivateChat() (Enter) and
// Toggle() (/hellion) restore it. Pure window-state — the focus side is left to smoke. // Toggle() (/hellion) restore it. Pure window-state — the focus side is left to smoke.
internal sealed class HideRestoreSelfTestStep : ISelfTestStep internal sealed class HideRestoreSelfTestStep : ISelfTestStep
{ {
+2 -2
View File
@@ -4,7 +4,7 @@ using HellionChat.Ui.Windows;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B1-2 window flags. Drives the REAL MainWindow.PreDraw and asserts it wired // window flags. Drives the REAL MainWindow.PreDraw and asserts it wired
// Window.Flags to ResolveFlags(CanMove, CanResize), then pins the pure // Window.Flags to ResolveFlags(CanMove, CanResize), then pins the pure
// fresh-base contract: false/false adds NoMove|NoResize, true/true clears them // fresh-base contract: false/false adds NoMove|NoResize, true/true clears them
// -- flags must rebuild from a fresh base, or NoMove sticks after toggling // -- flags must rebuild from a fresh base, or NoMove sticks after toggling
@@ -72,7 +72,7 @@ internal sealed class MainWindowFlagsStep : ISelfTestStep
return SelfTestStepResult.Fail; return SelfTestStepResult.Fail;
} }
// P7 title-bar contract: ShowTitleBar=false adds NoTitleBar from the // title-bar contract: ShowTitleBar=false adds NoTitleBar from the
// fresh base, true clears it (same no-accumulation guarantee). // fresh base, true clears it (same no-accumulation guarantee).
var barHidden = MainWindow.ResolveFlags(true, true, false); var barHidden = MainWindow.ResolveFlags(true, true, false);
var barShown = MainWindow.ResolveFlags(true, true, true); var barShown = MainWindow.ResolveFlags(true, true, true);
@@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// UI-12 focus opacity. Pins the pure ResolveBgAlpha contract (focused → // focus opacity. Pins the pure ResolveBgAlpha contract (focused →
// WindowOpacity, unfocused → WindowOpacityInactive). The PreDraw wiring // WindowOpacity, unfocused → WindowOpacityInactive). The PreDraw wiring
// (BgAlpha = ResolveBgAlpha(IsFocused) behind the main-viewport/!docked guard) // (BgAlpha = ResolveBgAlpha(IsFocused) behind the main-viewport/!docked guard)
// is NOT headless-deterministic — the guard may leave BgAlpha null when // is NOT headless-deterministic — the guard may leave BgAlpha null when
@@ -7,7 +7,7 @@ using HellionChat.Util;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B3-3: notification-sound selection. Drives the pure SelectNotificationSound // Notification-sound selection. Drives the pure SelectNotificationSound
// (the exact pick logic ProcessMessage runs per message) through its SelfTest // (the exact pick logic ProcessMessage runs per message) through its SelfTest
// wrapper with local synthetic tabs — Plugin.Config.Tabs is never touched, so // wrapper with local synthetic tabs — Plugin.Config.Tabs is never touched, so
// no real tab gains messages or unread state. The audible preview button is // no real tab gains messages or unread state. The audible preview button is
@@ -3,7 +3,7 @@ using System.IO;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// Disk sink for the B5 performance baseline. Kept separate from the SelfTest // Disk sink for the performance baseline. Kept separate from the SelfTest
// step so the per-frame hot path never references file IO. Writes one // step so the per-frame hot path never references file IO. Writes one
// perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move) // perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move)
// like ThemeRegistry's theme writer, so a mid-write crash leaves either the // like ThemeRegistry's theme writer, so a mid-write crash leaves either the
@@ -18,7 +18,7 @@ internal sealed class PerformanceBaselineStep : ISelfTestStep
private const int TargetFrames = 1000; private const int TargetFrames = 1000;
// Rough draw-call proxy: ImGui emits 6 indices per quad, so vertices/6 is an // Rough draw-call proxy: ImGui emits 6 indices per quad, so vertices/6 is an
// intentional under-count of draw work, not the exact quad count (API-3). // intentional under-count of draw work, not the exact quad count.
private const int VerticesPerQuadProxy = 6; private const int VerticesPerQuadProxy = 6;
private readonly Plugin _plugin; private readonly Plugin _plugin;
@@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B3-5: only the snap decision is headless-testable. Scroll detection + bar + // Only the snap decision is headless-testable. Scroll detection + bar +
// hit-test are smoke-only (the scroll child exists only in-game; GetScrollY is // hit-test are smoke-only (the scroll child exists only in-game; GetScrollY is
// garbage headless). Drives ResolveSnapToBottom via the SelfTest accessor and // garbage headless). Drives ResolveSnapToBottom via the SelfTest accessor and
// asserts the OR + the request reset invariant. // asserts the OR + the request reset invariant.
@@ -5,7 +5,7 @@ using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B2-1/B2-2: proves the WorldSuffixMode/NameFormMode reformat reaches the REAL // Proves the WorldSuffixMode/NameFormMode reformat reaches the REAL
// render entry. Drives ChunkRenderer.DrawChunks (a SelfTests/README-sanctioned // render entry. Drives ChunkRenderer.DrawChunks (a SelfTests/README-sanctioned
// real entry that wires SenderNameDisplay.ForDisplay at ChunkRenderer.cs:54) // real entry that wires SenderNameDisplay.ForDisplay at ChunkRenderer.cs:54)
// with a synthetic ChunkSource.Sender chunk carrying a PlayerPayload, at a // with a synthetic ChunkSource.Sender chunk carrying a PlayerPayload, at a
@@ -4,7 +4,7 @@ using HellionChat.Code;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// v1.10.0/C3: the active row gets a surface and an accent bar, so exactly the // v1.10.0: the active row gets a surface and an accent bar, so exactly the
// row the user is on must be marked -- and only that one. Drives the real // row the user is on must be marked -- and only that one. Drives the real
// Sidebar.Draw and reads the render-observability counter, so a regression in // Sidebar.Draw and reads the render-observability counter, so a regression in
// the draw path fails rather than a parallel calculation passing. // the draw path fails rather than a parallel calculation passing.
@@ -5,7 +5,7 @@ using HellionChat.GameFunctions.Types;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B3-2: greeted glyph renders only for temp tabs when the toggle is on. Drives // Greeted glyph renders only for temp tabs when the toggle is on. Drives
// the REAL Sidebar.Draw (render precedent: HonorificHeaderRenderStep, the only // the REAL Sidebar.Draw (render precedent: HonorificHeaderRenderStep, the only
// real .Draw in this pool — NOT SidebarModeAutoSwitchStep which only calls // real .Draw in this pool — NOT SidebarModeAutoSwitchStep which only calls
// IsExpanded/GetWidth) inside the /xlperf window frame and reads the render // IsExpanded/GetWidth) inside the /xlperf window frame and reads the render
@@ -58,7 +58,7 @@ internal sealed class SidebarModeAutoSwitchStep : ISelfTestStep
return SelfTestStepResult.Fail; return SelfTestStepResult.Fail;
} }
// B1-3a: the expanded width must come from Config.SidebarWidth, not the // The expanded width must come from Config.SidebarWidth, not the
// old fixed 150 constant. Drive the REAL GetWidth (the single source // old fixed 150 constant. Drive the REAL GetWidth (the single source
// Sidebar.Draw consumes) with concrete values and assert the OBSERVED // Sidebar.Draw consumes) with concrete values and assert the OBSERVED
// effect — in-range passthrough plus clamping — instead of mirroring the // effect — in-range passthrough plus clamping — instead of mirroring the
@@ -5,7 +5,7 @@ using HellionChat.GameFunctions.Types;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B3-4: section headers render once per non-empty temp-tab pool, and compact // Section headers render once per non-empty temp-tab pool, and compact
// mode suppresses the header text (separators stay). Drives the REAL // mode suppresses the header text (separators stay). Drives the REAL
// Sidebar.Draw inside the /xlperf window frame (same render precedent as // Sidebar.Draw inside the /xlperf window frame (same render precedent as
// SidebarGreetedGlyphStep) and reads the render observability counter. // SidebarGreetedGlyphStep) and reads the render observability counter.
@@ -4,7 +4,7 @@ using HellionChat.Ui.Components;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// B3-1: rename must persist. Drives the real ApplyTabRename (the InputText // Rename must persist. Drives the real ApplyTabRename (the InputText
// callback path), then SaveConfig + reload from disk and asserts the new name // callback path), then SaveConfig + reload from disk and asserts the new name
// survived — a fresh-from-config tab, not the same reference (a reference check // survived — a fresh-from-config tab, not the same reference (a reference check
// would pass on a dead roundtrip). Uses a persistent (non-temp) tab: unpinned // would pass on a dead roundtrip). Uses a persistent (non-temp) tab: unpinned
@@ -10,7 +10,7 @@ namespace HellionChat.SelfTests;
// The activation strip. Drives the REAL OnTabActivated -- the entry the // The activation strip. Drives the REAL OnTabActivated -- the entry the
// Sidebar/TopTabBar click handlers, the pop-out path and the Draw-seed all call // Sidebar/TopTabBar click handlers, the pop-out path and the Draw-seed all call
// — with local probe tabs (Plugin.Config.Tabs is never touched). Asserts the // — with local probe tabs (Plugin.Config.Tabs is never touched). Asserts the
// five contracts: strip-on-switch, no-strip-on-reclick (TR-4), leg1 preserve, // five contracts: strip-on-switch, no-strip-on-reclick, leg1 preserve,
// derive, and non-tell untouched. // derive, and non-tell untouched.
internal sealed class TellResetOnActivateStep : ISelfTestStep internal sealed class TellResetOnActivateStep : ISelfTestStep
{ {
@@ -41,7 +41,7 @@ internal sealed class TellResetOnActivateStep : ISelfTestStep
} }
// (b) re-clicking the already-active tab (previous == tab) must NOT strip // (b) re-clicking the already-active tab (previous == tab) must NOT strip
// a live game-tell conversation (TR-4 regression guard). // a live game-tell conversation (regression guard).
var reclick = MakeStaleTellTab(boundTellTarget: false, withLabel: false); var reclick = MakeStaleTellTab(boundTellTarget: false, withLabel: false);
TabLifecycleHelpers.OnTabActivated(reclick, reclick); TabLifecycleHelpers.OnTabActivated(reclick, reclick);
if (reclick.CurrentChannel.TellTarget is null) if (reclick.CurrentChannel.TellTarget is null)
@@ -4,7 +4,7 @@ using HellionChat.Themes;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// Verifies the v1.5.4 PM-1 crossfade contract: switching the active // Verifies the v1.5.4 crossfade contract: switching the active
// theme arms TryGetActiveCrossfade for ~300ms, then the registry // theme arms TryGetActiveCrossfade for ~300ms, then the registry
// returns to direct AbgrCache reads. A second switch within 100ms // returns to direct AbgrCache reads. A second switch within 100ms
// keeps the lerped path active (no identity-snap). CleanUp restores // keeps the lerped path active (no identity-snap). CleanUp restores
@@ -67,7 +67,7 @@ internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep
// it as "saw the start" if more than 300ms have elapsed. // it as "saw the start" if more than 300ms have elapsed.
// Skip the mid-crossfade-switch phase in that case -- the // Skip the mid-crossfade-switch phase in that case -- the
// lerped path is no longer active, so a second switch would // lerped path is no longer active, so a second switch would
// re-arm a fresh crossfade and not exercise PM-1b's // re-arm a fresh crossfade and not exercise its
// mid-flight-origin override. // mid-flight-origin override.
if (Environment.TickCount64 - this.armedAtTickMs > 300) if (Environment.TickCount64 - this.armedAtTickMs > 300)
{ {
@@ -83,7 +83,7 @@ internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep
if (!this.sawMidCrossfadeSwitch) if (!this.sawMidCrossfadeSwitch)
{ {
// PM-Test-3 mid-crossfade-switch phase: within ~100ms of the // mid-crossfade-switch phase: within ~100ms of the
// first observed crossfade, fire a second Switch to a THIRD // first observed crossfade, fire a second Switch to a THIRD
// theme. ArmCrossfade must compose the current lerped state // theme. ArmCrossfade must compose the current lerped state
// as the new origin -- TryGetActiveCrossfade still returns // as the new origin -- TryGetActiveCrossfade still returns
+1 -1
View File
@@ -4,7 +4,7 @@ using HellionChat.Code;
namespace HellionChat.SelfTests; namespace HellionChat.SelfTests;
// v1.10.0/D1: the top-tab strip marks the active tab with a fill plus an accent // v1.10.0: the top-tab strip marks the active tab with a fill plus an accent
// underline. Drives the real TopTabBar.Draw and reads the render counter. // underline. Drives the real TopTabBar.Draw and reads the render counter.
// //
// "At most one", not "exactly one": the strip skips popped-out tabs, so zero // "At most one", not "exactly one": the strip skips popped-out tabs, so zero
+3 -3
View File
@@ -51,9 +51,9 @@ internal static class EventHorizon
ChatColors: new ThemeChatColors( ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint> new Dictionary<HellionChat.Code.ChatType, uint>
{ {
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen // Cosmic purple drift: the pale pastels take a lavender tint and
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta- // the accent channels (tell) pull towards magenta-violet. Channel
// Lila. Channel-Identität bleibt klar erkennbar. // identity stays readable throughout.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"), [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
@@ -51,8 +51,8 @@ internal static class ForgeMerchantman
ChatColors: new ThemeChatColors( ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint> new Dictionary<HellionChat.Code.ChatType, uint>
{ {
// Forge Merchantman — Patina-Tinte in Party/FC, Bernstein-Tinte in // Patina tint on party and free company, amber on yell, alliance
// Yell/Alliance/CustomEmote. Channel-identity bleibt voll erhalten. // and custom emotes. Channel identity is left fully intact.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0C060"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0C060"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#E8902C"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#E8902C"),
+2 -2
View File
@@ -51,8 +51,8 @@ internal static class HellionArctic
ChatColors: new ThemeChatColors( ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint> new Dictionary<HellionChat.Code.ChatType, uint>
{ {
// Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den // The FFXIV defaults with a restrained cyan tint on the blue
// blauen Channels (Party/FC). Channel-Identität bleibt klar. // channels (party, free company). Channel identity stays clear.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
+4 -3
View File
@@ -51,9 +51,10 @@ internal static class IndigoViolet
ChatColors: new ThemeChatColors( ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint> new Dictionary<HellionChat.Code.ChatType, uint>
{ {
// Indigo Violet — Lavender-Pink-Drift in Tell und LS6/7. Türkis- // Lavender-pink drift on tell and linkshells 6 and 7, countered by
// Mint-Aurora-Counter in Party/FC und LS4. Glitter-Gold in Yell. // a turquoise-mint aurora on party, free company and linkshell 4.
// Differenzierung zu Event Horizon: dunkler, dichter, Türkis statt Gold. // Glitter gold on yell. What sets it apart from Event Horizon:
// darker, denser, and turquoise where that one goes gold.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0E6FF"), [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0E6FF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D880"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D880"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
+3 -3
View File
@@ -51,9 +51,9 @@ internal static class MintGrove
ChatColors: new ThemeChatColors( ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint> new Dictionary<HellionChat.Code.ChatType, uint>
{ {
// Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie, // Nature-themed tint: honey amber across the yell family, a mint
// Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität // drift in novice network and linkshell. Tell keeps its pink so
// bleibt erhalten für Erkennbarkeit. // the channel stays recognisable.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"), [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"),
+3 -2
View File
@@ -51,8 +51,9 @@ internal static class NightBlue
ChatColors: new ThemeChatColors( ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint> new Dictionary<HellionChat.Code.ChatType, uint>
{ {
// Night Blue — Royal-Blue-Tinte in Party/FC, Bronze-Gold in Yell/ // Royal blue on party and free company, bronze gold on yell and
// Alliance. Channel-identity (Tell-Pink, NN-Lime) bleibt erhalten. // alliance. Channel identity is preserved -- tell stays pink,
// novice network stays lime.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFD060"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFD060"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
@@ -51,8 +51,9 @@ internal static class SynthwaveSunset
ChatColors: new ThemeChatColors( ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint> new Dictionary<HellionChat.Code.ChatType, uint>
{ {
// Synthwave Sunset — Magenta dominiert die warmen Channels (Yell/Shout/FC), // Magenta carries the warm channels (yell, shout, free company),
// Cyan dominiert die kühlen (Tell/Party). Neon-Akzente für Status-nahe Channels. // cyan the cool ones (tell, party). Neon accents on the status-
// adjacent channels.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0DFFF"), [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0DFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FF2D95"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FF2D95"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF6BB6"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF6BB6"),
+5 -5
View File
@@ -10,7 +10,7 @@ public sealed class ThemeRegistry
public const string DefaultSlug = HellionArctic.Slug; public const string DefaultSlug = HellionArctic.Slug;
// 1Hz throttle for the v1.4.8 B2 auto-refresh-on-active path. The // 1Hz throttle for the v1.4.8 auto-refresh-on-active path. The
// Plugin.Draw hook calls RefreshActiveIfStale every frame, but the // Plugin.Draw hook calls RefreshActiveIfStale every frame, but the
// actual File.GetLastWriteTimeUtc disk-stat only runs once per second // actual File.GetLastWriteTimeUtc disk-stat only runs once per second
// -- 60fps would otherwise mean 3600 stats/min on the same path (more // -- 60fps would otherwise mean 3600 stats/min on the same path (more
@@ -24,7 +24,7 @@ public sealed class ThemeRegistry
private readonly string? _customThemesDir; private readonly string? _customThemesDir;
private Theme _active; private Theme _active;
// v1.4.8 B2: source path of the currently active custom theme. Captured // v1.4.8: source path of the currently active custom theme. Captured
// at Switch() time so RefreshActiveIfStale does not have to reconstruct // at Switch() time so RefreshActiveIfStale does not have to reconstruct
// a filename from the slug -- custom theme filenames are not required // a filename from the slug -- custom theme filenames are not required
// to match the slug they declare in the JSON body. Null when the active // to match the slug they declare in the JSON body. Null when the active
@@ -33,7 +33,7 @@ public sealed class ThemeRegistry
private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs; private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs;
private DateTime _lastActiveStamp = DateTime.MinValue; private DateTime _lastActiveStamp = DateTime.MinValue;
// PM-1 crossfade state. Switch() captures the previous AbgrCache as a // crossfade state. Switch() captures the previous AbgrCache as a
// VALUE-COPY (not a Theme reference) -- the built-in singletons share // VALUE-COPY (not a Theme reference) -- the built-in singletons share
// their RecomputeAbgrCache identity, so a reference would mutate // their RecomputeAbgrCache identity, so a reference would mutate
// alongside the new active. _crossfadeStartTickMs == long.MinValue // alongside the new active. _crossfadeStartTickMs == long.MinValue
@@ -115,7 +115,7 @@ public sealed class ThemeRegistry
// ThemeImportExportRow opens this path via Process.Start. // ThemeImportExportRow opens this path via Process.Start.
public string? CustomThemesDir => _customThemesDir; public string? CustomThemesDir => _customThemesDir;
// Read-only enumeration of all built-in theme slugs. T2 ThemePickerCategoryStep // Read-only enumeration of all built-in theme slugs. ThemePickerCategoryStep
// diffs this set against ThemePicker.CategoryMapSlugs to enforce coverage. // diffs this set against ThemePicker.CategoryMapSlugs to enforce coverage.
public IEnumerable<string> BuiltinSlugs => _builtIns.Keys; public IEnumerable<string> BuiltinSlugs => _builtIns.Keys;
@@ -293,7 +293,7 @@ public sealed class ThemeRegistry
// (see `Switch` built-in-first lookup), so saving a custom file under // (see `Switch` built-in-first lookup), so saving a custom file under
// a built-in slug persists the file but leaves the built-in active — // a built-in slug persists the file but leaves the built-in active —
// looks green, behaves broken. ColorPicker DrawIdleState forks // looks green, behaves broken. ColorPicker DrawIdleState forks
// built-in themes into a custom slug before BeginEditing, M6 // built-in themes into a custom slug before BeginEditing,
// ImportFromPath renames built-in-colliding imports to <slug>_imported. // ImportFromPath renames built-in-colliding imports to <slug>_imported.
// New call-sites must either fork first or rename to a non-built-in slug. // New call-sites must either fork first or rename to a non-built-in slug.
public bool SaveEditingBuffer(out string targetPath) public bool SaveEditingBuffer(out string targetPath)
+1 -1
View File
@@ -1,6 +1,6 @@
namespace HellionChat.Themes; namespace HellionChat.Themes;
// Pure stale-check for the v1.4.8 B2 theme-auto-refresh-on-active path. // Pure stale-check for the v1.4.8 theme-auto-refresh-on-active path.
// Lives in a free helper class so the Build-Suite can exercise the diff // Lives in a free helper class so the Build-Suite can exercise the diff
// rules without instantiating ThemeRegistry (which touches the Dalamud // rules without instantiating ThemeRegistry (which touches the Dalamud
// log proxy and the filesystem). The rules: // log proxy and the filesystem). The rules:
+1 -1
View File
@@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace HellionChat.Ui.Components; namespace HellionChat.Ui.Components;
// B2 (PERF-B2): variable-height clip plan. ImGuiListClipper needs a constant // Variable-height clip plan. ImGuiListClipper needs a constant
// row height, and since v1.10.0 neither density has one (compact rows wrap // row height, and since v1.10.0 neither density has one (compact rows wrap
// too), so both compute a plan from the cached per-row heights: a lead dummy // too), so both compute a plan from the cached per-row heights: a lead dummy
// for the rows above // for the rows above
+3 -3
View File
@@ -34,11 +34,11 @@ internal sealed class ChunkRenderer
// names change every plugin reload to avoid stable cross-session linkage. // names change every plugin reload to avoid stable cross-session linkage.
_salt = new Random().Next().ToString(); _salt = new Random().Next().ToString();
// Not yet consumed in C2/C3; E-task wiring will likely add log call-sites later. // No call sites yet; logging here will likely come later.
_ = _logger; _ = _logger;
} }
// B2-1/B2-2 render-observability: the formatted sender text the real draw // render-observability: the formatted sender text the real draw
// path actually produced (post-ForDisplay). A SelfTest reads this after // path actually produced (post-ForDisplay). A SelfTest reads this after
// driving DrawChunks to prove the WorldSuffixMode/NameFormMode reformat // driving DrawChunks to prove the WorldSuffixMode/NameFormMode reformat
// reached the real render entry — never the helper in isolation. null until // reached the real render entry — never the helper in isolation. null until
@@ -52,7 +52,7 @@ internal sealed class ChunkRenderer
float lineWidth = 0f float lineWidth = 0f
) )
{ {
// UI-7: render a copy with the sender name reformatted per the user's // Render a copy with the sender name reformatted per the user's
// display options. Skipped in screenshot mode so the name-anonymising // display options. Skipped in screenshot mode so the name-anonymising
// path in DrawChunk stays reliable (privacy wins). ForDisplay returns // path in DrawChunk stays reliable (privacy wins). ForDisplay returns
// the list unchanged when nothing applies, so non-sender lists and the // the list unchanged when nothing applies, so non-sender lists and the
+3 -3
View File
@@ -64,7 +64,7 @@ internal sealed class InputBar
private bool _wasInputTextHovered; private bool _wasInputTextHovered;
private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value. private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value.
// UI-11 plugin-disclosure arm-and-hold: holds the buffer that armed the // plugin-disclosure arm-and-hold: holds the buffer that armed the
// disclosure warning. null = not armed. Compared by value so an edit // disclosure warning. null = not armed. Compared by value so an edit
// re-arms and a resend on the identical buffer goes through. 1.5.6 parity // re-arms and a resend on the identical buffer goes through. 1.5.6 parity
// (ChatInputBar 1d3b429:27). // (ChatInputBar 1d3b429:27).
@@ -196,7 +196,7 @@ internal sealed class InputBar
ImGui.SameLine(); ImGui.SameLine();
DrawQuickButtons(); DrawQuickButtons();
// UI-11: yellow inline warning while a plugin-only-glyph message is // Yellow inline warning while a plugin-only-glyph message is
// armed-and-held (buffer unchanged since it armed). Renders on its own // armed-and-held (buffer unchanged since it armed). Renders on its own
// line below the input row. 1.5.6 parity (ChatInputBar 1d3b429:93-103). // line below the input row. 1.5.6 parity (ChatInputBar 1d3b429:93-103).
if ( if (
@@ -589,7 +589,7 @@ internal sealed class InputBar
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
return; return;
// UI-11: plugin-disclosure arm-and-hold. Arm + scan on the RAW // Plugin-disclosure arm-and-hold. Arm + scan on the RAW
// _pendingMessage (NOT the trimmed `text`) so the Draw warning gate // _pendingMessage (NOT the trimmed `text`) so the Draw warning gate
// (_pendingMessage == _disclosureArmedBuffer) matches byte-for-byte even // (_pendingMessage == _disclosureArmedBuffer) matches byte-for-byte even
// when the buffer has leading/trailing whitespace. 1.5.6 armed/held/ // when the buffer has leading/trailing whitespace. 1.5.6 armed/held/
+10 -10
View File
@@ -18,13 +18,13 @@ internal sealed class MessageList
private PayloadHandler? _handler; private PayloadHandler? _handler;
// B3-5: scroll-to-bottom state. Per-instance, so pop-out windows (own // Scroll-to-bottom state. Per-instance, so pop-out windows (own
// MessageList instance, PluginHostFactory.cs:263-266) isolate automatically — // MessageList instance, PluginHostFactory.cs:263-266) isolate automatically —
// the old 1.5.6 updateScrollState flag is NOT needed here. // the old 1.5.6 updateScrollState flag is NOT needed here.
private bool _scrolledUp; private bool _scrolledUp;
private bool _scrollToBottomRequested; private bool _scrollToBottomRequested;
// B2: the height cache is only valid while these inputs are unchanged. // The height cache is only valid while these inputs are unchanged.
// FontManager's own fingerprint covers font sizes only, not density / the two // FontManager's own fingerprint covers font sizes only, not density / the two
// name-display modes / width — a stale height would misplace the clipper dummies. // name-display modes / width — a stale height would misplace the clipper dummies.
// Per tab, not per list: the old single field let a width change in tab A mark // Per tab, not per list: the old single field let a width change in tab A mark
@@ -73,12 +73,12 @@ internal sealed class MessageList
return snap; return snap;
} }
// SelfTest hook (B3-5 reset-invariant, REQUIRED — not optional). Lets // SelfTest hook (reset-invariant, REQUIRED — not optional). Lets
// ScrollSnapDecisionStep flip the request flag without a real click, so the // ScrollSnapDecisionStep flip the request flag without a real click, so the
// post-snap reset can be asserted; without it only the OR branch is testable. // post-snap reset can be asserted; without it only the OR branch is testable.
internal void RequestScrollToBottomForSelfTest() => _scrollToBottomRequested = true; internal void RequestScrollToBottomForSelfTest() => _scrollToBottomRequested = true;
// SelfTest hook (B2): runs the real planner against a caller fixture so the // SelfTest hook: runs the real planner against a caller fixture so the
// step asserts the plan without a live scroll child (GetScrollY is garbage headless). // step asserts the plan without a live scroll child (GetScrollY is garbage headless).
internal CardClipPlan PlanCardClipForSelfTest( internal CardClipPlan PlanCardClipForSelfTest(
IReadOnlyList<float> heights, IReadOnlyList<float> heights,
@@ -86,7 +86,7 @@ internal sealed class MessageList
float viewportHeight float viewportHeight
) => CardClipPlanner.Plan(heights, scrollY, viewportHeight); ) => CardClipPlanner.Plan(heights, scrollY, viewportHeight);
// SelfTest hook (B2): drives the live invalidation, returns the tab's remaining // SelfTest hook: drives the live invalidation, returns the tab's remaining
// cached-height count so the step can assert the drop. nowMs is a parameter so // cached-height count so the step can assert the drop. nowMs is a parameter so
// the step can step past the settle window without sleeping (v1.10.0). // the step can step past the settle window without sleeping (v1.10.0).
internal int RunHeightCacheInvalidationForSelfTest(Tab tab, float contentWidth, long nowMs) internal int RunHeightCacheInvalidationForSelfTest(Tab tab, float contentWidth, long nowMs)
@@ -165,7 +165,7 @@ internal sealed class MessageList
MeasureTimestampColumn(tab); MeasureTimestampColumn(tab);
// B2: drop stale cached heights before the snapshot draw. Both densities // Drop stale cached heights before the snapshot draw. Both densities
// need this now -- compact rows are not constant height either, they wrap. // need this now -- compact rows are not constant height either, they wrap.
// Width read here while it is valid. // Width read here while it is valid.
InvalidateHeightCacheIfLayoutChanged( InvalidateHeightCacheIfLayoutChanged(
@@ -187,7 +187,7 @@ internal sealed class MessageList
var frozen = _fingerprintGates[tab.Identifier].IsPending; var frozen = _fingerprintGates[tab.Identifier].IsPending;
DrawRows(tab, messages, compact ? _drawCompactRow : _drawCardRow, frozen); DrawRows(tab, messages, compact ? _drawCompactRow : _drawCardRow, frozen);
// B3-5: scroll values are frame-constant inside the child, so this // Scroll values are frame-constant inside the child, so this
// reflects the current frame's state wherever it runs; kept after the // reflects the current frame's state wherever it runs; kept after the
// render to mirror the 1.5.6 end-of-DrawMessageLog placement. // render to mirror the 1.5.6 end-of-DrawMessageLog placement.
_scrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f; _scrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f;
@@ -282,7 +282,7 @@ internal sealed class MessageList
ImGui.SetCursorPos(origin with { X = origin.X + _stampColumnWidth }); ImGui.SetCursorPos(origin with { X = origin.X + _stampColumnWidth });
} }
// B3-5: Discord-style full-width bar pinned to the bottom edge of the // Discord-style full-width bar pinned to the bottom edge of the
// visible region while the user is scrolled up. Geometry comes from window // visible region while the user is scrolled up. Geometry comes from window
// pos + size (visible region), never from the content flow: when scrolled // pos + size (visible region), never from the content flow: when scrolled
// up the visible bottom sits above the content bottom, so the // up the visible bottom sits above the content bottom, so the
@@ -335,7 +335,7 @@ internal sealed class MessageList
private void DrawCompactRow(Message message, string? previousStamp) private void DrawCompactRow(Message message, string? previousStamp)
{ {
// B2-1/B2-2: render the sender through DrawChunks (the name-aware path // Render the sender through DrawChunks (the name-aware path
// that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a // that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a
// flat SenderSource.TextValue string. message.Sender already carries the // flat SenderSource.TextValue string. message.Sender already carries the
// channel brackets/colon as ChunkSource.None wrappers (MessageManager // channel brackets/colon as ChunkSource.None wrappers (MessageManager
@@ -566,7 +566,7 @@ internal sealed class MessageList
private void DrawCardRow(Message message, string? previousStamp) private void DrawCardRow(Message message, string? previousStamp)
{ {
// B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line // Sender via DrawChunks (name-aware path), on its own line
// with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no // with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no
// SameLine after the sender). The 1.5.6 channel-colour push on the // SameLine after the sender). The 1.5.6 channel-colour push on the
// sender is deferred styling polish (deferred to v1.9.0); plain // sender is deferred styling polish (deferred to v1.9.0); plain
@@ -133,7 +133,7 @@ internal sealed class AboutTab
LastHonorificStatusKey = kind.ToString(); LastHonorificStatusKey = kind.ToString();
var colors = _themes.Active.Colors; var colors = _themes.Active.Colors;
// Null-safety via the `is { } v` pattern, never `.Value` raw (spec SEC-2): // Null-safety via the `is { } v` pattern, never `.Value` raw :
// the version is bound only on the arms that have it; the impossible // the version is bound only on the arms that have it; the impossible
// Detected/Incompatible-without-version state falls through to default. // Detected/Incompatible-without-version state falls through to default.
switch (kind) switch (kind)
@@ -29,7 +29,7 @@ internal sealed class ThemePicker
(HellionStrings.Settings_Theme_Category_Retro, new[] { "synthwave-sunset" }, false), (HellionStrings.Settings_Theme_Category_Retro, new[] { "synthwave-sunset" }, false),
]; ];
// T2 ThemePickerCategoryStep diffs this against ThemeRegistry.BuiltinSlugs // ThemePickerCategoryStep diffs this against ThemeRegistry.BuiltinSlugs
// to enforce coverage. Kept on the static map so the test does not pierce instance state. // to enforce coverage. Kept on the static map so the test does not pierce instance state.
internal static IEnumerable<string> CategoryMapSlugs => CategoryMap.SelectMany(c => c.Slugs); internal static IEnumerable<string> CategoryMapSlugs => CategoryMap.SelectMany(c => c.Slugs);
+6 -6
View File
@@ -22,7 +22,7 @@ internal sealed class Sidebar
{ {
public const float IconOnlyWidth = 38f; public const float IconOnlyWidth = 38f;
// B1-3a: expanded sidebar width is user-configurable (Config.SidebarWidth), // Expanded sidebar width is user-configurable (Config.SidebarWidth),
// clamped to these bounds (matches the ChannelsTab slider range). Replaces // clamped to these bounds (matches the ChannelsTab slider range). Replaces
// the old fixed 150px ExpandedWidth constant. // the old fixed 150px ExpandedWidth constant.
public const float MinSidebarWidth = 40f; public const float MinSidebarWidth = 40f;
@@ -38,7 +38,7 @@ internal sealed class Sidebar
// previously active one -- that row was still active when it was painted. // previously active one -- that row was still active when it was painted.
internal int LastRenderedActiveSurfaceCount { get; private set; } internal int LastRenderedActiveSurfaceCount { get; private set; }
// B3-2 render observability: counts greeted glyphs actually drawn this frame. // render observability: counts greeted glyphs actually drawn this frame.
// Incremented ONLY in the real glyph branch in DrawRow; reset at Draw start. // Incremented ONLY in the real glyph branch in DrawRow; reset at Draw start.
// The SelfTest reads it after driving the real Draw — no dead service roundtrip. // The SelfTest reads it after driving the real Draw — no dead service roundtrip.
internal int LastRenderedGreetedGlyphCount; internal int LastRenderedGreetedGlyphCount;
@@ -48,7 +48,7 @@ internal sealed class Sidebar
// beside it. // beside it.
private const float PinGlyphScale = 0.6f; private const float PinGlyphScale = 0.6f;
// B3-4 render observability: section headers actually drawn this frame. // render observability: section headers actually drawn this frame.
// Incremented only in the real header branch; reset at Draw start. // Incremented only in the real header branch; reset at Draw start.
internal int LastDrawnSectionHeaderCount; internal int LastDrawnSectionHeaderCount;
@@ -149,7 +149,7 @@ internal sealed class Sidebar
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim); var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
var dl = ImGui.GetWindowDrawList(); var dl = ImGui.GetWindowDrawList();
// B3-4 sectioned render order (1.5.6 parity): persistent → pinned // sectioned render order (1.5.6 parity): persistent → pinned
// TempTabs → unpinned TempTabs. Only the display sequence regroups; // TempTabs → unpinned TempTabs. Only the display sequence regroups;
// the tab list itself stays untouched and every row keeps its // the tab list itself stays untouched and every row keeps its
// ORIGINAL list index for PushID, so an open context-menu popup // ORIGINAL list index for PushID, so an open context-menu popup
@@ -250,7 +250,7 @@ internal sealed class Sidebar
// Only split off a separate pop-out hit area when there's room for // Only split off a separate pop-out hit area when there's room for
// both buttons. Below that, the whole row stays as a single // both buttons. Below that, the whole row stays as a single
// selectable strip without the pop-out affordance. // selectable strip without the pop-out affordance.
// A3: gate the pop-out affordance on the expanded sidebar too. In // Gate the pop-out affordance on the expanded sidebar too. In
// icon-only mode avail still clears the width threshold, which used to // icon-only mode avail still clears the width threshold, which used to
// paint the pop-out glyph over the tab icon. The row stays a single // paint the pop-out glyph over the tab icon. The row stays a single
// selectable strip when collapsed; right-click pop-out is unaffected. // selectable strip when collapsed; right-click pop-out is unaffected.
@@ -476,7 +476,7 @@ internal sealed class Sidebar
// The hit area sits at the LEFT edge of the row, but the item must // The hit area sits at the LEFT edge of the row, but the item must
// be submitted AFTER TabContextMenu.Draw — any interactive item // be submitted AFTER TabContextMenu.Draw — any interactive item
// between the row button and the popup call would steal the // between the row button and the popup call would steal the
// right-click trigger (B3-1 ordering constraint). // right-click trigger (ordering constraint).
ImGui.SetCursorScreenPos(origin); ImGui.SetCursorScreenPos(origin);
// CheckCircle = greeted, plain Check = still pending (1.5.6 mapping). // CheckCircle = greeted, plain Check = still pending (1.5.6 mapping).
+1 -1
View File
@@ -82,7 +82,7 @@ internal static class TabContextMenu
ClearPendingRename(); ClearPendingRename();
} }
// Per-tab notification sound (B3-3). The checkbox gates the picker so // Per-tab notification sound. The checkbox gates the picker so
// tabs that never want a sound keep the popup short. // tabs that never want a sound keep the popup short.
if ( if (
ImGui.Checkbox( ImGui.Checkbox(
+2 -2
View File
@@ -36,7 +36,7 @@ public class DbViewer : Window
private int CurrentPage = 1; private int CurrentPage = 1;
private string SimpleSearchTerm = ""; private string SimpleSearchTerm = "";
// v1.4.8 H2: opt-in full-text search across the whole DB via FTS5. // v1.4.8: opt-in full-text search across the whole DB via FTS5.
// Transient UI state (per-session), not persisted -- users opt in fresh // Transient UI state (per-session), not persisted -- users opt in fresh
// every time so they always see the page-filter as the default mode. // every time so they always see the page-filter as the default mode.
private bool UseFullTextSearch; private bool UseFullTextSearch;
@@ -232,7 +232,7 @@ public class DbViewer : Window
tooltipRight: Language.Page_ArrowRight_Tooltip tooltipRight: Language.Page_ArrowRight_Tooltip
); );
// Full-text search toggle (v1.4.8 H2). IsFtsIndexBuilt is a cached // Full-text search toggle (v1.4.8). IsFtsIndexBuilt is a cached
// volatile bool in MessageStore -- single field read per frame, no // volatile bool in MessageStore -- single field read per frame, no
// SELECT count(*). ImRaii.Disabled blocks any click while the index // SELECT count(*). ImRaii.Disabled blocks any click while the index
// is still being built, so no defensive force-off branch needed // is still being built, so no defensive force-off branch needed
+1 -1
View File
@@ -168,7 +168,7 @@ internal sealed class InputPreview : Window
{ {
ImGui.TextUnformatted(Language.Options_Preview_Header); ImGui.TextUnformatted(Language.Options_Preview_Header);
// Primary path (A2) resets the Lender counter in MainWindow.Draw(); // Primary path resets the Lender counter in MainWindow.Draw();
// this fallback covers the edge-case where MainWindow is closed but // this fallback covers the edge-case where MainWindow is closed but
// InputPreview is still open, preventing handler pool growth. // InputPreview is still open, preventing handler pool growth.
if (!_mainWindow.IsOpen) if (!_mainWindow.IsOpen)
+1 -1
View File
@@ -44,7 +44,7 @@ internal sealed class ChannelPopoutPool
// A popped tab gets its own input bar, so strip stale tell state first — // A popped tab gets its own input bar, so strip stale tell state first —
// otherwise a popped-out stale-tell tab would be a send surface that // otherwise a popped-out stale-tell tab would be a send surface that
// bypasses the click-path activation strip. Previous = the main window's // bypasses the click-path activation strip. Previous = the main window's
// active tab; popping the active tab itself must not strip (TR-4 guard). // active tab; popping the active tab itself must not strip (guard).
TabLifecycleHelpers.OnTabActivated(tab, Plugin.Instance.MainWindow?.ActiveTab); TabLifecycleHelpers.OnTabActivated(tab, Plugin.Instance.MainWindow?.ActiveTab);
var slot = _slots.TryReserve(tab.Identifier); var slot = _slots.TryReserve(tab.Identifier);
@@ -103,7 +103,7 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
} }
// IFocusableChatWindow — this pop-out's own InputBar carries the focus state // IFocusableChatWindow — this pop-out's own InputBar carries the focus state
// the keybind tail checks when deciding whether to route at this surface (C3). // the keybind tail checks when deciding whether to route at this surface.
public bool HasFocusedInput => _input.IsFocused; public bool HasFocusedInput => _input.IsFocused;
// Arm-and-hold the one-frame Activate flag; the pop-out's Draw applies the // Arm-and-hold the one-frame Activate flag; the pop-out's Draw applies the
+3 -3
View File
@@ -82,14 +82,14 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
RespectCloseHotkey = false; RespectCloseHotkey = false;
} }
// UI-12: per-window focus-dependent opacity. ResolveBgAlpha stays guard-free // Per-window focus-dependent opacity. ResolveBgAlpha stays guard-free
// and pure so the self-test can drive it directly; PreDraw owns the guard + // and pure so the self-test can drive it directly; PreDraw owns the guard +
// wiring. 1.5.6 parity (focused → WindowOpacity, unfocused → // wiring. 1.5.6 parity (focused → WindowOpacity, unfocused →
// WindowOpacityInactive, ChatLogWindow.PreOpenCheck 1d3b429:724). // WindowOpacityInactive, ChatLogWindow.PreOpenCheck 1d3b429:724).
internal float ResolveBgAlpha(bool isFocused) => internal float ResolveBgAlpha(bool isFocused) =>
isFocused ? Plugin.Config.WindowOpacity : Plugin.Config.WindowOpacityInactive; isFocused ? Plugin.Config.WindowOpacity : Plugin.Config.WindowOpacityInactive;
// B1-2 / P7: rebuild flags from a fresh base every frame so toggling // Rebuild flags from a fresh base every frame so toggling
// CanMove/CanResize/ShowTitleBar back on actually CLEARS NoMove/NoResize/ // CanMove/CanResize/ShowTitleBar back on actually CLEARS NoMove/NoResize/
// NoTitleBar (not accumulating). Move/resize/title-bar logic as 1.5.6 // NoTitleBar (not accumulating). Move/resize/title-bar logic as 1.5.6
// (ChatLogWindow.PreOpenCheck 1d3b429:703-710); base flags = today's // (ChatLogWindow.PreOpenCheck 1d3b429:703-710); base flags = today's
@@ -240,7 +240,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
} }
// IFocusableChatWindow — the keybind tail resolves which surface owns the // IFocusableChatWindow — the keybind tail resolves which surface owns the
// input focus before routing a channel-set/REPLY/prefill at it (C3). // input focus before routing a channel-set/REPLY/prefill at it.
public bool HasFocusedInput => _input.IsFocused; public bool HasFocusedInput => _input.IsFocused;
// Arm-and-hold: field writes only, safe from the framework thread; the draw // Arm-and-hold: field writes only, safe from the framework thread; the draw
+1 -1
View File
@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.Windows; namespace HellionChat.Ui.Windows;
// `internal` to match the Plugin.SettingsWindow property in W2; `public` here // `internal` to match the Plugin.SettingsWindow property; `public` here
// would raise CS0053 against the internal members. Matches MainWindow shape. // would raise CS0053 against the internal members. Matches MainWindow shape.
internal sealed class SettingsWindow : Window internal sealed class SettingsWindow : Window
{ {
+2 -2
View File
@@ -91,7 +91,7 @@ internal static class ColourUtil
} }
// Modulates the alpha byte of an ABGR color by a factor in [0, 1]. // Modulates the alpha byte of an ABGR color by a factor in [0, 1].
// RGB stays intact. Used by the PM-3 hover-lerp path where each // RGB stays intact. Used by the hover-lerp path where each
// frame produces a fractional alpha value but the colour itself // frame produces a fractional alpha value but the colour itself
// must not shift. // must not shift.
internal static uint ApplyAlpha(uint abgr, float alphaFactor) internal static uint ApplyAlpha(uint abgr, float alphaFactor)
@@ -103,7 +103,7 @@ internal static class ColourUtil
} }
// Mixes an ABGR colour's RGB channels toward white (0xFF) by factor t in // Mixes an ABGR colour's RGB channels toward white (0xFF) by factor t in
// [0, 1]; the alpha byte is left untouched. A1 hover-sheen accent-tint: // [0, 1]; the alpha byte is left untouched. Hover-sheen accent tint:
// a low factor nudges the sweep toward the element's accent hue without // a low factor nudges the sweep toward the element's accent hue without
// going fully saturated (effect level stays "subtle"). RGB-only on // going fully saturated (effect level stays "subtle"). RGB-only on
// purpose -- DrawHoverSheen owns the alpha falloff. // purpose -- DrawHoverSheen owns the alpha falloff.
+3 -4
View File
@@ -90,10 +90,9 @@ internal static class ImGuiUtil
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled])) using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
ImGui.TextUnformatted("(?)"); ImGui.TextUnformatted("(?)");
// AllowWhenDisabled — ohne das Flag liefert IsItemHovered bei // AllowWhenDisabled, because without it IsItemHovered returns false on a
// ausgegrauten Settings false, der User könnte nicht mehr lesen // greyed-out setting -- and a reader who cannot find out why an option is
// warum eine Option nicht aktiv ist. Genau dann braucht er den // inactive is exactly the reader who needs the tooltip most.
// Hover-Tooltip aber am dringendsten.
if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
return; return;
+2 -2
View File
@@ -4,7 +4,7 @@ using HellionChat._Helpers;
namespace HellionChat.Util; namespace HellionChat.Util;
// UI-7: produces a render-only view of a chunk list with the sender's name // Produces a render-only view of a chunk list with the sender's name
// reformatted per the user's WorldSuffixMode / NameFormMode. Called from // reformatted per the user's WorldSuffixMode / NameFormMode. Called from
// ChatLogWindow.DrawChunks on every draw — it never mutates the input, so the // ChatLogWindow.DrawChunks on every draw — it never mutates the input, so the
// stored message (the Sender BLOB in the DB) stays byte-for-byte unchanged and // stored message (the Sender BLOB in the DB) stays byte-for-byte unchanged and
@@ -76,7 +76,7 @@ internal static class SenderNameDisplay
// text, and any cross-world icon) with one formatted chunk that keeps // text, and any cross-world icon) with one formatted chunk that keeps
// the PlayerPayload link so the name stays clickable. Dropping the // the PlayerPayload link so the name stays clickable. Dropping the
// original sender icon is an accepted trade-off and only happens once // original sender icon is an accepted trade-off and only happens once
// the user moves UI-7 off its defaults. // the user moves off its defaults.
// Channel brackets and colons are ChunkSource.None wrappers outside the // Channel brackets and colons are ChunkSource.None wrappers outside the
// Sender span — they are preserved untouched by the copy loops below. // Sender span — they are preserved untouched by the copy loops below.
var copy = new List<Chunk>(chunks.Count); var copy = new List<Chunk>(chunks.Count);
+2 -2
View File
@@ -72,7 +72,7 @@ internal static class TabLifecycleHelpers
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t); public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
// GP-04: clear every Tab.PopOut at load time. The pool binds later, so at // Clear every Tab.PopOut at load time. The pool binds later, so at
// load NO tab can own a slot — a persisted PopOut=true is always a stale flag // load NO tab can own a slot — a persisted PopOut=true is always a stale flag
// with no window. Unconditional (pinned included) because pinned TempTabs // with no window. Unconditional (pinned included) because pinned TempTabs
// survive the load and are the main stale-flag source; a !IsPinned filter // survive the load and are the main stale-flag source; a !IsPinned filter
@@ -91,7 +91,7 @@ internal static class TabLifecycleHelpers
// partner-name label) so a normal typed line cannot route as a silent /tell // partner-name label) so a normal typed line cannot route as a silent /tell
// to the old partner — the same privacy guard StripTellBindingOnPromote // to the old partner — the same privacy guard StripTellBindingOnPromote
// applies on promote. Re-activating the already-active tab must NOT strip // applies on promote. Re-activating the already-active tab must NOT strip
// (a live game-tell would lose its context, TR-4); a tab carrying its own // (a live game-tell would lose its context); a tab carrying its own
// Tab.TellTarget is a real tell binding (leg1) and is left intact. // Tab.TellTarget is a real tell binding (leg1) and is left intact.
internal static void OnTabActivated(Tab tab, Tab? previous) internal static void OnTabActivated(Tab tab, Tab? previous)
{ {
@@ -1,6 +1,6 @@
namespace HellionChat._Helpers; namespace HellionChat._Helpers;
// UI-11 pure decision helper: does a message about to be sent carry a glyph // pure decision helper: does a message about to be sent carry a glyph
// that only renders correctly for players running HellionChat or a similar // that only renders correctly for players running HellionChat or a similar
// plugin? Those are FFXIV Private-Use-Area icon codepoints (the same range // plugin? Those are FFXIV Private-Use-Area icon codepoints (the same range
// SeIconChar covers); a recipient without a plugin sees an empty box. // SeIconChar covers); a recipient without a plugin sees an empty box.
+1 -1
View File
@@ -2,7 +2,7 @@ using System;
namespace HellionChat._Helpers; namespace HellionChat._Helpers;
// UI-7 pure decision helper: builds the display string for a sender's name + // pure decision helper: builds the display string for a sender's name +
// world per the user's NameFormMode / WorldSuffixMode. Dalamud-free so the // world per the user's NameFormMode / WorldSuffixMode. Dalamud-free so the
// Build Suite can cover every combination; SenderNameDisplay feeds it the name // Build Suite can cover every combination; SenderNameDisplay feeds it the name
// and world it pulled from the PlayerPayload. // and world it pulled from the PlayerPayload.