chore: code quality sweep 2026-05-04 / 2026-05-05

General code-quality and robustness pass across the plugin: thread-
safety on IPC state, resource-disposal cleanups, input validation,
defensive null-checks and a few small UX glitches. Compliance docs
(THIRD_PARTY_NOTICES, PRIVACY, COPYRIGHT) refreshed to v1.0.3.

Highlights
- ExtraChat IPC state synchronised across threads
- ChatLogWindow autocomplete no longer leaks the unmanaged
  ImGuiListClipper allocation
- ChatLogWindow + Popout style stack stays balanced when config
  toggles mid-frame
- Retention sweep and privacy cleanup wait for the actual filter
  pass instead of the fire-and-forget Task that started it
- Configuration.LatestVersion bumped to 13 to match the active
  migration path
- GameFunctions placeholder buffer guarded against oversized
  replacement names
- TellTarget.IsSet, ResolveTempInputChannel, InputPreview, IconUtil,
  Lender, Payloads, ExtraPayload all hardened against null / empty /
  EOF / cycle inputs
- FontManager Lodestone download stays in scope for a follow-up
  (timeout + lazy init pending)
- AutoTranslate replaced the msvcrt.dll memcmp P/Invoke with a
  managed Span comparison
- Privacy cleanup worker thread marked IsBackground = true
- Database cleanup now removes both legacy files in one click
- Tell-target name redacted in the verbose debug log

Compliance
- THIRD_PARTY_NOTICES: last-reviewed bumped to v1.0.3, Pidgin 3.5.1,
  SQLitePCLRaw.lib.e_sqlite3 3.50.3 listed as direct dependency with
  CVE-2025-6965 / CVE-2025-7709 rationale
- PRIVACY: last-reviewed bumped to v1.0.3, BetterTTV trigger wording
  clarified (list fetch at startup vs. on-demand image fetch)
- COPYRIGHT: upstream attribution range widened

Build: 0 warnings, 0 errors. No behavioural changes that would alter
existing user configuration or stored chat history.
This commit is contained in:
2026-05-05 07:25:47 +02:00
parent 698eb01bbe
commit 4d54eabdac
26 changed files with 251 additions and 98 deletions
+86 -56
View File
@@ -34,6 +34,9 @@ public sealed class ChatLogWindow : Window
internal Plugin Plugin { get; }
private readonly CommandWrapper _clearHellionCommand;
private readonly CommandWrapper _hellionCommand;
internal bool ScreenshotMode;
private string Salt { get; }
@@ -110,8 +113,14 @@ public sealed class ChatLogWindow : Window
SetUpTextCommandChannels();
SetUpAllCommands();
Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log").Execute += ClearLog;
Plugin.Commands.Register("/hellion").Execute += ToggleChat;
// Cache the registered wrapper instances so Dispose can detach the same
// event objects the constructor attached to, without going through
// Register() again (which would re-create the wrapper if the command
// happened to be missing from the dictionary).
_clearHellionCommand = Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log");
_hellionCommand = Plugin.Commands.Register("/hellion");
_clearHellionCommand.Execute += ClearLog;
_hellionCommand.Execute += ToggleChat;
Plugin.ClientState.Login += Login;
Plugin.ClientState.Logout += Logout;
@@ -126,8 +135,8 @@ public sealed class ChatLogWindow : Window
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
Plugin.ClientState.Logout -= Logout;
Plugin.ClientState.Login -= Login;
Plugin.Commands.Register("/hellion").Execute -= ToggleChat;
Plugin.Commands.Register("/clearhellion").Execute -= ClearLog;
_hellionCommand.Execute -= ToggleChat;
_clearHellionCommand.Execute -= ClearLog;
}
private void Logout(int _, int __)
@@ -514,13 +523,28 @@ public sealed class ChatLogWindow : Window
return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout;
}
// Tracks the style instance pushed in PreDraw so PostDraw can pop the same
// one even if the user toggled OverrideStyle / ChosenStyle mid-frame.
// Without this, a config change between PreDraw and PostDraw could either
// leak a Push (no matching Pop) or pop nothing while we still have a frame
// pushed onto the ImGui stack.
private StyleModel? _pushedStyle;
public override void PreDraw()
{
if (Plugin.Config.KeepInputFocus && Activate)
ImGui.SetWindowFocus(WindowName);
_pushedStyle = null;
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
{
var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle);
if (style != null)
{
style.Push();
_pushedStyle = style;
}
}
}
public override void PostDraw()
@@ -532,8 +556,11 @@ public sealed class ChatLogWindow : Window
if (Plugin.CurrentTab.InputDisabled)
Activate = false;
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
if (_pushedStyle != null)
{
_pushedStyle.Pop();
_pushedStyle = null;
}
}
public override void OnClose()
@@ -597,10 +624,11 @@ public sealed class ChatLogWindow : Window
Plugin.InputPreview.CalculatePreview();
// Hellion Chat v0.6.1 — render the one-time hint banner first so it
// sits above the tab area / sidebar in full window width. Stash the
// height for GetRemainingHeightForMessageLog so the message log
// shrinks accordingly while the banner is visible.
_v061HintBannerHeight = DrawV061HintBannerIfNeeded();
// sits above the tab area / sidebar in full window width. ImGui's
// GetContentRegionAvail subtracts its height automatically because the
// cursor advances past it before the message log calls
// GetRemainingHeightForMessageLog, so we don't track the height here.
DrawV061HintBannerIfNeeded();
if (Plugin.Config.SidebarTabView)
DrawTabSidebar();
@@ -1540,11 +1568,14 @@ public sealed class ChatLogWindow : Window
var startY = ImGui.GetCursorPosY();
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
ImGui.PushStyleColor(ImGuiCol.ChildBg, bg);
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
var dismiss = false;
var openSettings = false;
// RAII for the style stack so an early return in this block
// (or a later refactor that introduces one) can never leave the
// ImGui style stack unbalanced. Matches the convention used
// elsewhere in this file.
using (ImRaii.PushColor(ImGuiCol.ChildBg, bg))
using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1f))
using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true))
{
if (child)
@@ -1561,8 +1592,6 @@ public sealed class ChatLogWindow : Window
}
}
ImGui.PopStyleVar();
ImGui.PopStyleColor();
ImGui.Spacing();
if (dismiss)
@@ -1636,13 +1665,6 @@ public sealed class ChatLogWindow : Window
internal readonly List<bool> PopOutDocked = [];
internal readonly HashSet<Guid> PopOutWindows = [];
// Hellion Chat v0.6.1 — height the v0.6.1 hint banner consumed in the
// current frame, read by GetRemainingHeightForMessageLog so the message
// log can shrink. Unconditionally reassigned at the top of DrawChatLog
// (before any tab-area render) so the value is always in sync with the
// current frame. Returns 0 once the banner is dismissed.
private float _v061HintBannerHeight;
// v0.6.0 — live enumeration of all active Popout windows so the
// KeybindManager can find a focused ChatInputBar to forward tab-cycle
// keybinds to. Filter on IsOpen prevents touching closed-but-still-
@@ -1745,47 +1767,55 @@ public sealed class ChatLogWindow : Window
return;
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper());
clipper.Begin(AutoCompleteList.Count);
while (clipper.Step())
try
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
clipper.Begin(AutoCompleteList.Count);
while (clipper.Step())
{
var entry = AutoCompleteList[i];
var highlight = AutoCompleteSelection == i;
var clicked = ImGui.Selectable($"{entry.Text}##{entry.Group}/{entry.Row}", highlight) || selected == i;
if (i < 10)
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
var button = (i + 1) % 10;
var text = string.Format(Language.AutoTranslate_Completion_Key, button);
var size = ImGui.CalcTextSize(text);
var entry = AutoCompleteList[i];
ImGui.SameLine(ImGui.GetContentRegionAvail().X - size.X);
var highlight = AutoCompleteSelection == i;
var clicked = ImGui.Selectable($"{entry.Text}##{entry.Group}/{entry.Row}", highlight) || selected == i;
if (i < 10)
{
var button = (i + 1) % 10;
var text = string.Format(Language.AutoTranslate_Completion_Key, button);
var size = ImGui.CalcTextSize(text);
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
ImGui.TextUnformatted(text);
ImGui.SameLine(ImGui.GetContentRegionAvail().X - size.X);
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
ImGui.TextUnformatted(text);
}
if (!clicked)
continue;
var before = Chat[..AutoCompleteInfo.StartPos];
var after = Chat[AutoCompleteInfo.EndPos..];
var replacement = $"<at:{entry.Group},{entry.Row}>";
Chat = $"{before}{replacement}{after}";
ImGui.CloseCurrentPopup();
Activate = true;
ActivatePos = AutoCompleteInfo.StartPos + replacement.Length;
}
if (!clicked)
continue;
var before = Chat[..AutoCompleteInfo.StartPos];
var after = Chat[AutoCompleteInfo.EndPos..];
var replacement = $"<at:{entry.Group},{entry.Row}>";
Chat = $"{before}{replacement}{after}";
ImGui.CloseCurrentPopup();
Activate = true;
ActivatePos = AutoCompleteInfo.StartPos + replacement.Length;
}
if (!AutoCompleteShouldScroll)
return;
AutoCompleteShouldScroll = false;
var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f);
ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y);
}
finally
{
// ImGuiListClipperPtr wraps an unmanaged ImGuiListClipper allocated above.
// Without Destroy() the unmanaged block leaks per autocomplete render.
clipper.Destroy();
}
if (!AutoCompleteShouldScroll)
return;
AutoCompleteShouldScroll = false;
var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f);
ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y);
}
private int AutoCompleteCallback(scoped ref ImGuiInputTextCallbackData data)