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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user