Merge feature/v1.4.10 — Symbol-Picker and Tell-History Fix

This commit is contained in:
2026-05-16 14:04:20 +02:00
16 changed files with 586 additions and 112 deletions
+36
View File
@@ -0,0 +1,36 @@
---
subtitle: Symbol-Picker und Tell-History Fix
versionsnatur: Feature-Patch + Hotfix
---
- Symbol-Picker im Chat-Eingang: ein kleiner Smile-Button links neben
dem Kanal-Indikator öffnet ein Popup mit zwei Tabs. Der erste listet
alle 161 FFXIV-PUA-Glyphen (Dalamuds SeIconChar); der zweite trägt
97 verifizierte BMP-Symbole (Latin-Marken, Währungen, das ganze
griechische Alphabet, Geometrie, Spielkarten, Noten) — jedes davon
über `/echo` und `/say` in einer vierrundigen Whitelist-Probe
durchgereicht, damit der Channel-Render dem entspricht, was der
Picker anzeigt. Klick fügt das Symbol an der Cursor-Position ein,
Multi-Insert lässt das Popup offen, eine Recent-Used-Leiste zeigt
die letzten sechzehn Picks über beide Tabs. Toggle in Settings →
Chat → Nachrichten-Verhalten, Default an.
- Verlauf in angepinnten Tell-Tabs lädt wieder vollständig: ein
versteckter 500-Zeilen-Scan-Cap in PreloadHistory hat das
User-Setting `AutoTellTabsHistoryPreload` überschrieben, wodurch
weniger-frequente Tell-Partner ihren Backlog verloren haben sobald
die Scan-Schicht mit anderen Chat-Partnern voll lief. Cap ist raus,
der Index auf `(Receiver, Date)` hält die Query schnell.
- Slash-Command-Teardown: /hellion, /hellionView, /hellionDebugger
(und im Debug-Build /hellionSeString) sind als private Felder
gecached. Plugin-Dispose detached die echte Registrierung, statt
mit identischen Args neu zu registrieren — schließt eine latente
Wartungs-Falle aus v1.4.9.
- v1.4.x-Polish-Sweep endet hier. Der ImGuiListClipper-Refactor von
der v1.4.10-Reserve-Liste wurde gecancelt, nachdem der Cross-
Plattform-Smoke gezeigt hat dass das Scroll-Gummi ein Wine/Linux-
Quirk ist — Windows-User haben es nie gesehen. Spike dafür kommt in
einem späteren Patch. Nächster Major-Cycle ist v1.5.0 mit der
DI-Container-Adoption (`Microsoft.Extensions.Hosting` +
`ILogger<T>`) nach dem Lightless-Vorbild.
- Migration v17 unverändert: kein Schema-Bump, kein
Config-Migrations-Aufwand.
+2
View File
@@ -176,6 +176,7 @@ public class Configuration : IPluginConfiguration
public bool SortAutoTranslate;
public bool CollapseDuplicateMessages;
public bool CollapseKeepUniqueLinks;
public bool SymbolPickerEnabled = true;
public bool PlaySounds = true;
public bool KeepInputFocus = true;
public int MaxLinesToRender = 2_500; // 1-10000
@@ -270,6 +271,7 @@ public class Configuration : IPluginConfiguration
SortAutoTranslate = other.SortAutoTranslate;
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
SymbolPickerEnabled = other.SymbolPickerEnabled;
PlaySounds = other.PlaySounds;
KeepInputFocus = other.KeepInputFocus;
MaxLinesToRender = other.MaxLinesToRender;
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
<PropertyGroup>
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
<Version>1.4.9</Version>
<Version>1.4.10</Version>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Use lock file to pin exact versions -->
+44 -39
View File
@@ -35,6 +35,50 @@ tags:
- Replacement
- Privacy
changelog: |-
**v1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)**
Eleventh and final sub-patch of the v1.4.x polish-sweep series.
Symbol picker for the chat input, a tell-history reload fix for
users with many active partners, and a closing cleanup sweep
before v1.5.0 picks up the DI-container adoption.
- Symbol picker: a small smile-icon button left of the channel
indicator opens a popup with two tabs. The first lists all 161
FFXIV PUA glyphs (Dalamud's SeIconChar enum); the second
carries 97 server-verified BMP symbols (latin marks, currency,
the full Greek alphabet, geometric shapes, suits, notes) —
every one of them round-tripped through /echo and /say in a
four-round probe so the in-channel render matches what the
picker shows. Click drops the glyph at the caret, multi-insert
keeps the popup open, and a recent-used strip floats the last
sixteen picks across both tabs. Toggle in Settings → Chat →
Message behaviour, default on.
- Pinned auto-tell tabs reload their full history again: a
hidden 500-row scan cap in PreloadHistory used to override the
user-configurable AutoTellTabsHistoryPreload setting, so
less-frequent pinned partners (rare /tell sessions in an
otherwise busy week) lost their backlog. The cap is removed;
the (Receiver, Date) index keeps SQL fast, the client-side
loop still respects your setting as the upper bound.
- Slash-command teardown: /hellion, /hellionView,
/hellionDebugger (and #if DEBUG /hellionSeString) wrappers are
now cached as private fields. Plugin teardown detaches the
live registration instead of re-Register'ing with identical
args — closes a latent maintenance hazard from v1.4.9.
- v1.4.x polish-sweep wraps up here. The ImGuiListClipper render
refactor that was on the v1.4.10 reserve list got dropped
after cross-platform smoke showed the scroll rubber-band is a
Wine / Linux render-pipeline quirk, not universal — Windows
users never saw it. It will get its own platform-targeted
spike in a later patch. Next major cycle is v1.5.0 with the
DI-container adoption (Microsoft.Extensions.Hosting +
ILogger<T>) modelled on Lightless.
- Migration v17 stays (no schema bump).
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
**v1.4.9 — Plugin-Load Render Polish (2026-05-15)**
Tenth sub-patch of the v1.4.x polish-sweep series. First-frame
@@ -150,43 +194,4 @@ changelog: |-
---
**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**
Maintenance patch. No user-visible behaviour changes; tightens the
development feedback loop, fixes two upstream-inherited bugs, and
prepares the code for the v1.4.7 backlog cleanup.
- preflight.sh gains a csharpier reflow check and a markdownlint
pass so style drift and markdown violations are caught at the
pre-push gate
- FontManager fallback catches the full set of atlas-toolkit
throws (IO, InvalidOperation, ArgumentException) — a corrupt
font config no longer takes down the whole atlas build
- BrandingLinks and IntegrationLinks URLs validated on plugin
load — a typo in a future URL rotation now throws at startup
- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel
no longer leaks the native Utf8String when the linkshell check
rejects the channel
- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now
deep-clones UsedChannel and TellTarget — PopOut and Temp tabs
no longer mutate each other's channel state
- Active-tab underline scales with DPI and rounds to physical
pixels for crisp rendering above 100% scaling
- IconButton width parameter no longer subtracts HUD-scaled
padding from a raw int (measured width passes through verbatim)
- Internal: HellionStyle ChildBgAlpha extracted to a testable
helper; Plugin.SaveConfig clones only the temp tabs;
SettingsOverview caches the draw-list per frame;
Dalamud.Utility.Util surface routed through an IPlatformUtil
indirection (MessageStore IsWine probe is now testable in
isolation)
- Built-in themes: Crystal Nocturne (sapphire and electric
magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom.
Users with Moonlit Bloom selected fall back to Hellion Arctic
on first load
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
+8 -9
View File
@@ -997,16 +997,17 @@ internal class MessageStore : IDisposable
}
}
// Returns up to limit tells exchanged with the named player, oldest-first.
// SQL narrows by Receiver + ChatType (indexed); client does the final
// PlayerPayload comparison. sqlScanLimit caps the scan to stay within
// the message-processing worker thread budget.
// Returns up to `limit` tells exchanged with the named player, oldest-first.
// SQL narrows by Receiver + ChatType via the (Receiver, Date) index, then
// the client-side loop runs PlayerPayload comparison and breaks once
// `limit` partner matches accumulate. Earlier versions had a hardcoded
// 500-row scan cap that cut less-frequent pinned partners off the back of
// the window in chatty sessions; removed in v1.4.10.
internal IReadOnlyList<Message> GetTellHistoryWithSender(
ulong receiver,
string senderName,
uint senderWorld,
int limit,
int sqlScanLimit = 500
int limit
)
{
if (limit <= 0)
@@ -1024,14 +1025,12 @@ internal class MessageStore : IDisposable
WHERE deleted = false
AND Receiver = $Receiver
AND ChatType IN ($TellIncoming, $TellOutgoing)
ORDER BY Date DESC
LIMIT $ScanLimit;
ORDER BY Date DESC;
";
cmd.CommandTimeout = 60;
cmd.Parameters.AddWithValue("$Receiver", receiver);
cmd.Parameters.AddWithValue("$TellIncoming", (int)ChatType.TellIncoming);
cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing);
cmd.Parameters.AddWithValue("$ScanLimit", sqlScanLimit);
var collected = new List<Message>();
using var enumerator = new MessageEnumerator(cmd.ExecuteReader(), _logger);
+52 -28
View File
@@ -123,6 +123,15 @@ public sealed class Plugin : IAsyncDalamudPlugin
// isolation. Wired immediately after Dalamud injects Log.
internal static IPluginLogProxy LogProxy { get; private set; } = null!;
// Wrapper cached so TearDown can detach the live instance instead of
// re-registering with identical args (v1.4.9 ISSUE-1 cleanup).
private CommandWrapper? _hellionSettingsCmd;
private CommandWrapper? _hellionViewCmd;
private CommandWrapper? _hellionDebuggerCmd;
#if DEBUG
private CommandWrapper? _hellionSeStringCmd;
#endif
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
private int _disposeStarted;
@@ -189,8 +198,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (Config.Version < 16)
{
throw new InvalidOperationException(
$"HellionChat v1.4.9 requires config schema v16, got v{Config.Version}. "
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.9."
$"HellionChat v1.4.10 requires config schema v16, got v{Config.Version}. "
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10."
);
}
Config.Version = 17;
@@ -699,21 +708,25 @@ public sealed class Plugin : IAsyncDalamudPlugin
{
// ChatLogWindow.cs:128 already registers /hellion (ToggleChat). The
// description-arg here keeps the Dalamud help list populated.
Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute +=
OnHellionSettingsCommand;
Commands
.Register(
"/hellionView",
"Get access to your message history, with simple filter options.",
true
)
.Execute += OnHellionViewCommand;
Commands.Register("/hellionDebugger", showInHelp: false).Execute +=
OnHellionDebuggerCommand;
_hellionSettingsCmd = Commands.Register(
"/hellion",
"Perform various actions with Hellion Chat."
);
_hellionSettingsCmd.Execute += OnHellionSettingsCommand;
_hellionViewCmd = Commands.Register(
"/hellionView",
"Get access to your message history, with simple filter options.",
true
);
_hellionViewCmd.Execute += OnHellionViewCommand;
_hellionDebuggerCmd = Commands.Register("/hellionDebugger", showInHelp: false);
_hellionDebuggerCmd.Execute += OnHellionDebuggerCommand;
#if DEBUG
// SeStringDebugger.cs lives under #if DEBUG too; keep this out of release builds.
Commands.Register("/hellionSeString", showInHelp: false).Execute +=
OnHellionSeStringCommand;
_hellionSeStringCmd = Commands.Register("/hellionSeString", showInHelp: false);
_hellionSeStringCmd.Execute += OnHellionSeStringCommand;
#endif
// Plugin-Manager "Settings" button. Was in Settings.cs:67 pre-v1.4.9.
@@ -729,20 +742,31 @@ public sealed class Plugin : IAsyncDalamudPlugin
Interface.UiBuilder.OpenMainUi -= OnOpenMainUi;
Interface.UiBuilder.OpenConfigUi -= OnOpenConfigUi;
Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute -=
OnHellionSettingsCommand;
Commands
.Register(
"/hellionView",
"Get access to your message history, with simple filter options.",
true
)
.Execute -= OnHellionViewCommand;
Commands.Register("/hellionDebugger", showInHelp: false).Execute -=
OnHellionDebuggerCommand;
// Null-tolerant detaches: TearDownCommands can run from the LoadAsync
// failure path (Plugin.cs CaptureFailure) before SetupCommands finished.
if (_hellionSettingsCmd is not null)
{
_hellionSettingsCmd.Execute -= OnHellionSettingsCommand;
_hellionSettingsCmd = null;
}
if (_hellionViewCmd is not null)
{
_hellionViewCmd.Execute -= OnHellionViewCommand;
_hellionViewCmd = null;
}
if (_hellionDebuggerCmd is not null)
{
_hellionDebuggerCmd.Execute -= OnHellionDebuggerCommand;
_hellionDebuggerCmd = null;
}
#if DEBUG
Commands.Register("/hellionSeString", showInHelp: false).Execute -=
OnHellionSeStringCommand;
if (_hellionSeStringCmd is not null)
{
_hellionSeStringCmd.Execute -= OnHellionSeStringCommand;
_hellionSeStringCmd = null;
}
#endif
}
+4
View File
@@ -270,6 +270,10 @@ internal class HellionStrings
internal static string Settings_Chat_Preview_Heading => Get(nameof(Settings_Chat_Preview_Heading));
internal static string Settings_Chat_Emotes_Heading => Get(nameof(Settings_Chat_Emotes_Heading));
// Hellion Chat — Chat-Tab SymbolPicker
internal static string Settings_Chat_SymbolPicker_Enable_Name => Get(nameof(Settings_Chat_SymbolPicker_Enable_Name));
internal static string Settings_Chat_SymbolPicker_Enable_Description => Get(nameof(Settings_Chat_SymbolPicker_Enable_Description));
// Hellion Chat — Database-Tab section headings
internal static string Settings_Database_Storage_Heading => Get(nameof(Settings_Database_Storage_Heading));
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
@@ -556,6 +556,14 @@
<value>Emotes</value>
</data>
<!-- Hellion Chat — Chat-Tab SymbolPicker -->
<data name="Settings_Chat_SymbolPicker_Enable_Name" xml:space="preserve">
<value>Symbol-Picker-Button neben dem Chat-Eingang anzeigen</value>
</data>
<data name="Settings_Chat_SymbolPicker_Enable_Description" xml:space="preserve">
<value>Fügt einen kleinen Button links neben dem Kanal-Indikator ein. Klick öffnet ein Popup mit FFXIV-Glyphen und einer kuratierten Symbol-Liste. Ausschalten für eine schlankere Eingabezeile.</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Database-Tabs -->
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
<value>Speicherung</value>
@@ -556,6 +556,14 @@
<value>Emotes</value>
</data>
<!-- Hellion Chat — Chat tab SymbolPicker -->
<data name="Settings_Chat_SymbolPicker_Enable_Name" xml:space="preserve">
<value>Show symbol-picker button next to chat input</value>
</data>
<data name="Settings_Chat_SymbolPicker_Enable_Description" xml:space="preserve">
<value>Adds a small button left of the channel indicator that opens a popup with FFXIV icons and a curated symbol list. Disable if you prefer a leaner input bar.</value>
</data>
<!-- Hellion Chat — Database tab section headings -->
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
<value>Storage</value>
+37
View File
@@ -40,6 +40,7 @@ public sealed class ChatLogWindow : Window
private readonly CommandWrapper _clearHellionCommand;
private readonly CommandWrapper _hellionCommand;
private readonly SymbolPicker _symbolPicker;
internal bool ScreenshotMode;
private string Salt { get; }
@@ -129,6 +130,8 @@ public sealed class ChatLogWindow : Window
_clearHellionCommand.Execute += ClearLog;
_hellionCommand.Execute += ToggleChat;
_symbolPicker = new SymbolPicker();
Plugin.ClientState.Login += Login;
Plugin.ClientState.Logout += Logout;
@@ -792,6 +795,40 @@ public sealed class ChatLogWindow : Window
)
inputColour = ecColour;
// Symbol-picker trigger sits left of the channel indicator. ImRaii.Popup
// inside DrawAndConsume pins to the last rendered item, so the call MUST
// run immediately after this IconButton — placing it after the channel
// picker below would pin the popup under the wrong widget.
if (Plugin.Config.SymbolPickerEnabled)
{
if (
ImGuiUtil.IconButton(
FontAwesomeIcon.Smile,
"symbol-picker-trigger",
"Insert symbol or FFXIV icon"
)
)
{
_symbolPicker.OpenPopup();
}
}
// DrawAndConsume runs unconditionally; with the button hidden the popup
// can't open, so the call is a no-op. Splice path stays outside the
// guard for the same reason.
var insertedSymbol = _symbolPicker.DrawAndConsume();
if (insertedSymbol is not null)
{
// Same cursor-aware splice idiom as the AutoComplete commit path at
// ChatLogWindow.cs:2487-2493. Clamp because CursorPos can drift if
// the user mutates Chat while the popup is open.
var pos = Math.Clamp(CursorPos, 0, Chat.Length);
Chat = Chat[..pos] + insertedSymbol + Chat[pos..];
Activate = true;
ActivatePos = pos + insertedSymbol.Length;
}
if (Plugin.Config.SymbolPickerEnabled)
ImGui.SameLine();
var beforeIcon = ImGui.GetCursorPos();
var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
+6
View File
@@ -139,6 +139,12 @@ internal sealed class Chat : ISettingsTab
);
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMsgUniqueLink_Description);
}
ImGui.Checkbox(
HellionStrings.Settings_Chat_SymbolPicker_Enable_Name,
ref Mutable.SymbolPickerEnabled
);
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_SymbolPicker_Enable_Description);
}
}
+308
View File
@@ -0,0 +1,308 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text;
using Dalamud.Interface.Utility.Raii;
namespace HellionChat.Ui;
// Popup picker for chat-input symbol insertion. Two tabs:
// PUA — Dalamud's SeIconChar enum (161 server-safe FFXIV glyphs)
// BMP — server-verified Unicode symbols (whitelist built 2026-05-16)
//
// Render-only — the Settings-Guard for showing the trigger button lives on
// the caller side (ChatLogWindow). Recent-Used is session state by design.
internal sealed class SymbolPicker
{
private const string PopupId = "HellionSymbolPicker";
private const int RecentCapacity = 16;
private string _search = string.Empty;
private readonly List<uint> _recentUsed = new(capacity: RecentCapacity);
// FFXIV server-safe BMP symbols, verified 2026-05-16 via /echo + /say.
// Filtered ranges: U+2694-26C4 (Misc Symbols Extended), U+2700+ (Dingbats
// Extended), diagonal arrows, U+2153+ fractions, chess pieces.
// Full probe log: Cycles/v1.4.10 BMP-Whitelist Notes.md.
private static readonly (uint Codepoint, string Name)[] BmpWhitelist = new[]
{
(0x00A1u, "Inverted Exclamation"),
(0x00A2u, "Cent Sign"),
(0x00A3u, "Pound Sign"),
(0x00A4u, "Currency Sign"),
(0x00A5u, "Yen Sign"),
(0x00A7u, "Section Sign"),
(0x00A9u, "Copyright Sign"),
(0x00ABu, "Left Angle Quote"),
(0x00AEu, "Registered Sign"),
(0x00B0u, "Degree Sign"),
(0x00B1u, "Plus-Minus Sign"),
(0x00B6u, "Pilcrow Sign"),
(0x00BBu, "Right Angle Quote"),
(0x00BCu, "One Quarter"),
(0x00BDu, "One Half"),
(0x00BEu, "Three Quarters"),
(0x00BFu, "Inverted Question"),
(0x00D7u, "Multiplication Sign"),
(0x00F7u, "Division Sign"),
(0x0393u, "Greek Capital Gamma"),
(0x0394u, "Greek Capital Delta"),
(0x0398u, "Greek Capital Theta"),
(0x039Bu, "Greek Capital Lambda"),
(0x039Eu, "Greek Capital Xi"),
(0x03A0u, "Greek Capital Pi"),
(0x03A3u, "Greek Capital Sigma"),
(0x03A6u, "Greek Capital Phi"),
(0x03A8u, "Greek Capital Psi"),
(0x03A9u, "Greek Capital Omega"),
(0x03B1u, "Greek Small Alpha"),
(0x03B2u, "Greek Small Beta"),
(0x03B3u, "Greek Small Gamma"),
(0x03B4u, "Greek Small Delta"),
(0x03B5u, "Greek Small Epsilon"),
(0x03B6u, "Greek Small Zeta"),
(0x03B7u, "Greek Small Eta"),
(0x03B8u, "Greek Small Theta"),
(0x03B9u, "Greek Small Iota"),
(0x03BAu, "Greek Small Kappa"),
(0x03BBu, "Greek Small Lambda"),
(0x03BCu, "Greek Small Mu"),
(0x03BDu, "Greek Small Nu"),
(0x03BEu, "Greek Small Xi"),
(0x03BFu, "Greek Small Omicron"),
(0x03C0u, "Greek Small Pi"),
(0x03C1u, "Greek Small Rho"),
(0x03C3u, "Greek Small Sigma"),
(0x03C4u, "Greek Small Tau"),
(0x03C5u, "Greek Small Upsilon"),
(0x03C6u, "Greek Small Phi"),
(0x03C7u, "Greek Small Chi"),
(0x03C8u, "Greek Small Psi"),
(0x03C9u, "Greek Small Omega"),
(0x2013u, "En Dash"),
(0x2014u, "Em Dash"),
(0x2020u, "Dagger"),
(0x2021u, "Double Dagger"),
(0x2026u, "Horizontal Ellipsis"),
(0x203Bu, "Reference Mark"),
(0x20ACu, "Euro Sign"),
(0x2122u, "Trade Mark Sign"),
(0x2190u, "Leftwards Arrow"),
(0x2191u, "Upwards Arrow"),
(0x2192u, "Rightwards Arrow"),
(0x2193u, "Downwards Arrow"),
(0x21D2u, "Rightwards Double Arrow"),
(0x21D4u, "Left Right Double Arrow"),
(0x2202u, "Partial Differential"),
(0x2207u, "Nabla"),
(0x2211u, "Summation"),
(0x221Au, "Square Root"),
(0x221Eu, "Infinity"),
(0x222Bu, "Integral"),
(0x2260u, "Not Equal To"),
(0x25A0u, "Black Square"),
(0x25A1u, "White Square"),
(0x25B2u, "Black Up Triangle"),
(0x25B3u, "White Up Triangle"),
(0x25BCu, "Black Down Triangle"),
(0x25C6u, "Black Diamond"),
(0x25C7u, "White Diamond"),
(0x25CBu, "White Circle"),
(0x25CFu, "Black Circle"),
(0x2600u, "Black Sun With Rays"),
(0x2601u, "Cloud"),
(0x2602u, "Umbrella"),
(0x2603u, "Snowman"),
(0x2605u, "Black Star"),
(0x2606u, "White Star"),
(0x2640u, "Female Sign"),
(0x2642u, "Male Sign"),
(0x2660u, "Black Spade Suit"),
(0x2661u, "White Heart Suit"),
(0x2663u, "Black Club Suit"),
(0x2665u, "Black Heart Suit"),
(0x266Au, "Eighth Note"),
(0x2713u, "Check Mark"),
};
public void OpenPopup() => ImGui.OpenPopup(PopupId);
// Returns the inserted codepoint as a string fragment if the user clicked
// one this frame, or null otherwise. Caller splices the fragment into the
// chat-input buffer at the current cursor position.
public string? DrawAndConsume()
{
// ImRaii.Popup auto-disposes EndPopup, same idiom as other popups in
// ChatLogWindow.
using var popup = ImRaii.Popup(PopupId);
if (!popup)
return null;
string? inserted = null;
// Recent-Used-Row sits above the tabs so both PUA and BMP picks share
// one fast-access strip. Session-only by design (see TrackRecent).
if (_recentUsed.Count > 0)
{
ImGui.TextDisabled("Recent");
ImGui.SameLine();
foreach (var codepoint in _recentUsed)
{
var glyph = char.ConvertFromUtf32((int)codepoint);
if (
ImGui.Selectable(
glyph,
false,
ImGuiSelectableFlags.DontClosePopups,
new Vector2(20, 20)
)
)
{
inserted = glyph;
}
ImGui.SameLine();
}
ImGui.NewLine();
ImGui.Separator();
}
using (var tabs = ImRaii.TabBar("##symbolpicker-tabs"))
{
if (tabs)
{
inserted = DrawPuaTab() ?? inserted;
inserted = DrawBmpTab() ?? inserted;
}
}
if (inserted is not null)
TrackRecent(inserted);
return inserted;
}
private string? DrawPuaTab()
{
using var tab = ImRaii.TabItem("FFXIV Icons");
if (!tab)
return null;
ImGui.InputTextWithHint(
"##pua-search",
"Search by name (e.g. HighQuality)",
ref _search,
64
);
string? inserted = null;
if (ImGui.BeginChild("##pua-grid", new Vector2(0, 280), false))
{
var query = _search;
foreach (var icon in Enum.GetValues<SeIconChar>())
{
var label = icon.ToString();
if (
query.Length > 0
&& label.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0
)
{
continue;
}
// ToIconString gives the single-codepoint glyph; tooltip
// carries the enum name for discoverability.
if (
ImGui.Selectable(
icon.ToIconString(),
false,
ImGuiSelectableFlags.DontClosePopups,
new Vector2(24, 24)
)
)
{
inserted = icon.ToIconString();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(label);
// Manually-wrapping pattern from imgui_demo.cpp;
// GetWindowContentRegionMax obsolete since ImGui 1.92, use
// GetContentRegionAvail (see ChatLogWindow.cs:840).
var style = ImGui.GetStyle();
var lastItemX2 = ImGui.GetItemRectMax().X;
var availableRightX =
ImGui.GetCursorScreenPos().X + ImGui.GetContentRegionAvail().X;
if (lastItemX2 + style.ItemSpacing.X + 24f < availableRightX)
ImGui.SameLine();
}
}
ImGui.EndChild();
return inserted;
}
private string? DrawBmpTab()
{
using var tab = ImRaii.TabItem("Symbols");
if (!tab)
return null;
ImGui.InputTextWithHint("##bmp-search", "Search by name (e.g. Heart)", ref _search, 64);
string? inserted = null;
if (ImGui.BeginChild("##bmp-grid", new Vector2(0, 280), false))
{
var query = _search;
foreach (var (codepoint, name) in BmpWhitelist)
{
if (query.Length > 0 && name.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
var glyph = char.ConvertFromUtf32((int)codepoint);
if (
ImGui.Selectable(
glyph,
false,
ImGuiSelectableFlags.DontClosePopups,
new Vector2(24, 24)
)
)
{
inserted = glyph;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(name);
// Same manually-wrapping pattern as DrawPuaTab — modern API
// since GetWindowContentRegionMax was deprecated in ImGui 1.92.
var style = ImGui.GetStyle();
var lastItemX2 = ImGui.GetItemRectMax().X;
var availableRightX =
ImGui.GetCursorScreenPos().X + ImGui.GetContentRegionAvail().X;
if (lastItemX2 + style.ItemSpacing.X + 24f < availableRightX)
ImGui.SameLine();
}
}
ImGui.EndChild();
return inserted;
}
private void TrackRecent(string fragment)
{
if (string.IsNullOrEmpty(fragment) || fragment.Length > 4)
return;
var codepoint = (uint)char.ConvertToUtf32(fragment, 0);
// Move-to-front so the head stays the freshest pick.
_recentUsed.RemoveAll(c => c == codepoint);
_recentUsed.Insert(0, codepoint);
if (_recentUsed.Count > RecentCapacity)
_recentUsed.RemoveAt(_recentUsed.Count - 1);
}
}
+19 -20
View File
@@ -2,7 +2,7 @@
[![Build](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml/badge.svg?branch=main)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE)
[![Latest release](https://img.shields.io/badge/release-v1.4.9-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
[![Latest release](https://img.shields.io/badge/release-v1.4.10-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
[![Dalamud API](https://img.shields.io/badge/Dalamud-API_15-purple)](https://github.com/goatcorp/Dalamud)
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
[![FFXIV](https://img.shields.io/badge/FFXIV-Dawntrail-c3a37f)](https://www.finalfantasyxiv.com/)
@@ -11,7 +11,7 @@
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
</p>
**Version 1.4.9** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
**Version 1.4.10** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
@@ -286,24 +286,23 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
## Project Status
**Version 1.4.9**Plugin-Load Render Polish. First-frame render cost is now well under Dalamud's 100 ms HITCH
warning threshold (~76 ms median, down from ~127 ms). The gain comes from deferring six non-essential rendering
sections on the very first Draw — bottom status bar, channel-name SeString chunks, window bounds check, hint
banner, autocomplete and input-preview calculation — so the initial ImGui layout cost is spread between frame 0
and frame 1 instead of all hitting at once. At 60 fps the user sees those sections one frame (~17 ms) later, which
is invisible inside the post-reload font-atlas build window. Slash commands `/hellion`, `/hellionView`,
`/hellionSeString` and `/hellionDebugger` are now registered centrally during plugin load so they work before
their target window is opened the first time. The configuration-button entry in Dalamud's plugin manager hangs on
the same path. Three plugin-load profiling logs (auto-translate warm-up, message-store connect, tab filter) stay
on at Information level as a regression tripwire — if a future change pushes the load past 100 ms again, the cost
is right there in `/xllog`. The release also ships a ChatTwo IPC compatibility layer: HellionChat now mirrors
ChatTwo's full IPC surface (`GetChatInputState`, `ChatInputStateChanged`, `Register`, `Unregister`, `Available`,
`Invoke`) under the `ChatTwo.*` namespace in addition to our existing `HellionChat.*` provider gates, so
third-party integrations that historically only subscribe to ChatTwo's IPC (Artisan's and AllaganTools' context-
menu hooks are the practical examples) keep working without requiring a code change on their side. Conflict
detection prevents ChatTwo from loading in parallel with HellionChat, so there is no slot-collision risk at
runtime. Migration v17 stays (no schema bump). Tenth sub-patch of the v1.4.x polish sweep series (as of
2026-05-15).
**Version 1.4.10**Symbol-Picker and Tell-History Fix. Eleventh and final sub-patch of the v1.4.x polish sweep
series. A new symbol-picker popup hangs off a smile-icon button left of the channel indicator: tab one lists all
161 FFXIV PUA glyphs (Dalamud's `SeIconChar` enum); tab two carries 97 server-verified BMP symbols (latin marks,
currency, the full Greek alphabet, geometric shapes, suits, notes) — each one round-tripped through `/echo` and
`/say` in a four-round whitelist probe so the in-channel render matches what the picker shows. Click drops the
glyph at the caret, multi-insert keeps the popup open, recent-used strip floats the last sixteen picks across
both tabs. Toggle in Settings → Chat → Message behaviour, default on. Mid-cycle hotfix for pinned auto-tell tabs:
PreloadHistory had a hidden 500-row SQL scan cap that overrode the user-configurable `AutoTellTabsHistoryPreload`
setting — active users with many tell partners lost the backlog of less-frequent pinned partners. The cap is
removed; the `(Receiver, Date)` index keeps SQL fast, the client-side loop respects the user setting as the upper
bound. Slash-command teardown cleanup: `/hellion`, `/hellionView`, `/hellionDebugger` (and `#if DEBUG /hellionSeString`)
wrappers are cached as private fields so plugin teardown detaches the live registration instead of re-Register'ing
with identical args. The original Reserve-A `ImGuiListClipper` refactor for `DrawMessages` was cancelled after
cross-platform smoke showed the scroll rubber-band is a Wine/Linux render-pipeline quirk, not universal — Windows
users on v1.4.9 never saw it; the spike that targets the Wine path lives in a later patch. Migration v17 stays
(no schema bump). v1.4.x polish sweep wraps up here; next major cycle is v1.5.0 with the DI-container adoption
(`Microsoft.Extensions.Hosting` + `ILogger<T>`) modelled on Lightless (as of 2026-05-16).
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
+25
View File
@@ -10,6 +10,31 @@ to the release pages for details.
---
## Hellion Chat 1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)
Eleventh and final sub-patch of the v1.4.x Polish-Sweep series. Symbol picker for the chat input, a tell-history reload fix
for users with many active partners, and a closing cleanup sweep before v1.5.0 picks up the DI-container adoption.
- Symbol picker for the chat input: smile-icon button left of the channel indicator opens a popup with two tabs —
161 FFXIV PUA glyphs (Dalamud's SeIconChar enum) and 97 server-verified BMP symbols round-tripped through `/echo` and
`/say` in a four-round probe. Cursor-aware splice, multi-insert keeps the popup open, recent-used strip floats the last
sixteen picks across both tabs. Toggle in Settings → Chat → Message behaviour, default on.
- Pinned auto-tell tabs reload their full history again. PreloadHistory had a hidden 500-row scan cap that overrode the
user-configurable `AutoTellTabsHistoryPreload` setting whenever you chatted with many partners; less-frequent pinned
partners lost their backlog. The cap is removed.
- Slash-command teardown cleanup: `/hellion`, `/hellionView`, `/hellionDebugger` (and `#if DEBUG /hellionSeString`) wrappers
are now cached as private fields so plugin teardown detaches the live registration instead of re-Register'ing with
identical args (latent maintenance hazard from v1.4.9).
- v1.4.x Polish-Sweep wraps up here. The ImGuiListClipper render refactor that was on the v1.4.10 reserve list got dropped
after cross-platform smoke showed the scroll rubber-band is a Wine/Linux render-pipeline quirk, not universal — Windows
users never saw it. It will get its own platform-targeted spike in a later patch. Next major cycle is v1.5.0 with the
DI-container adoption (Microsoft.Extensions.Hosting + ILogger<T>) modelled on Lightless.
- Migration v17 stays (no schema bump).
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
## Hellion Chat 1.4.9 — Plugin-Load Render Polish (2026-05-15)
Tenth sub-patch of the v1.4.x polish-sweep series. First-frame render cost drops from ~127 ms median down to
+22 -9
View File
@@ -10,16 +10,29 @@ the plugin's privacy-first scope during brainstorming.
---
## Next Cycle (v1.4.10)
## Next Cycle (v1.5.0)
**Render Clipper, Symbol-Picker and Final-Cleanup.** Reserve items inherited from the v1.4.9 plan that did not need to
land in the HITCH-cut: an `ImGuiListClipper` for variable-height messages in `DrawMessages` (the OtterGui `ImGuiClip.cs`
wrapper is the idiom anchor), a Symbol Picker popup for the chat input (`imgui_demo.cpp` Popups & Modal Windows section
is the pattern reference), plus the carry-over from v1.4.9: structural First-Frame-Layout rewrite if the v1.4.9 selective
defers turn out to be too narrow once user-side regressions surface. Lazy-Window-Init naive is **not** in scope — the
v1.4.9 Stage-2 diagnose falsified that path (`WindowSystem.windows` is non-thread-safe, Game-Freeze under reload stress,
no measurable HITCH delta). A clean DI-container adoption (Lightless `PluginHostFactory` pattern) belongs in v1.5.x and
will revisit the question with the right threading model.
**DI-container adoption.** Microsoft.Extensions.Hosting plus `ILogger<T>` modelled on Lightless's `PluginHostFactory`
pattern. The v1.4.x Polish-Sweep series is closed; v1.5.0 starts the structural cycle that the smaller F12.x indirection
shims (`IPluginLogProxy`, `IPlatformUtil`) were paving the way for. After that, the Wine/Linux scroll-rubber-band spike
deferred from v1.4.10 (Reserve-A cancelled — Windows users never saw it) plus the First-Run-Wizard rework that lets users
opt into the curated defaults instead of just picking a privacy profile.
---
## v1.4.10 — Symbol-Picker and Tell-History Fix (released 2026-05-16)
Eleventh and final sub-patch of the v1.4.x Polish Sweep series. Symbol picker for the chat input — popup with two tabs
(161 FFXIV PUA glyphs via Dalamud's SeIconChar plus 97 server-verified BMP symbols probed through `/echo` and `/say` in
a four-round whitelist build) — cursor-aware splice, multi-insert, recent-used strip across both tabs, Settings toggle
in Chat → Message behaviour. Mid-cycle hotfix for pinned auto-tell tabs: PreloadHistory used to cap the SQL scan at
500 rows regardless of the user's `AutoTellTabsHistoryPreload` setting, so active users with many partners lost the
backlog of less-frequent pinned partners; the cap is gone, the `(Receiver, Date)` index keeps SQL fast, the client-side
loop respects the user setting as the upper bound. Slash-command teardown cleanup wires the v1.4.9 wrappers through
private fields so dispose detaches the live registration instead of re-registering with identical args. The original
Reserve-A `ImGuiListClipper` refactor for `DrawMessages` was cancelled after cross-platform smoke showed the scroll
rubber-band is a Wine/Linux render-pipeline quirk, not universal — Windows-side testing on v1.4.9 confirmed no lag.
Migration v17 stays.
---
+6 -6
View File
File diff suppressed because one or more lines are too long