Compare commits

..

32 Commits

Author SHA1 Message Date
JonKazama-Hellion 3be4e73c27 Merge feature/v1.4.10 — Symbol-Picker and Tell-History Fix
Forge Announce / Post changelog to Hellion Forge (push) Successful in 7s
Release / Build and attach release ZIP (push) Successful in 37s
2026-05-16 14:04:20 +02:00
JonKazama-Hellion 667950c98e docs: add v1.4.10 forge announcement post and apply csharpier reflow
Forge-Post is required in the tagged tree so forge-announce.yml can read
it during the release-pipeline run. Plus a csharpier reflow on two files
(SymbolPicker.cs, ChatLogWindow.cs) that preflight Block E flagged after
the cycle's comment-tightening sweep — purely whitespace, no behaviour
change.
2026-05-16 14:01:17 +02:00
JonKazama-Hellion 3e91177833 release: bump to v1.4.10 2026-05-16 13:25:53 +02:00
JonKazama-Hellion 51f18e46a0 chore(comments): tighten v1.4.10 inline commentary after self-review
Five trim spots from the cycle's earlier commits — none change behaviour,
just drop redundant phrasing and stale references per the HellionChat
comment-style convention (1-3 lines default, link "same as X" instead of
repeating, file:line refs only where they aid navigation).

SymbolPicker:
- BmpWhitelist header consolidated to source + filter ranges
- ImRaii.Popup pattern links the established ChatLogWindow popup idiom
  instead of citing three call-sites
- ToIconString comment drops the "discoverability" footnote that the
  code already telegraphs
- Manually-wrapping comment drops the "same modern idiom" tail that
  duplicated the preceding sentence

MessageStore:
- Merge the stale pre-v1.4.10 sqlScanLimit comment with the new
  v1.4.10 commentary; the cap mention now describes the historical
  reason rather than a parameter that no longer exists
2026-05-16 12:49:01 +02:00
JonKazama-Hellion f66316161b fix(autotells): preload tell history fully up to the user-configured limit
PreloadHistory had a hardcoded 500-row SQL scan window that capped the
per-partner history pull regardless of the AutoTellTabsHistoryPreload
setting. For active users with many tell partners, the scan window
filled up with chatter from other partners and pushed less-frequent
partners' history off the back end — pinned tabs reloaded empty even
though the messages were still in the database.

Drops the hardcoded scan cap. The (Receiver, Date) index keeps SQL fast
on the now-unbounded read, and the client-side loop still breaks as
soon as the configured per-tab limit is hit, so decode cost stays
proportional to the depth at which `limit` matches accumulate (typically
shallow even for chatty users).
2026-05-16 12:16:08 +02:00
JonKazama-Hellion 679b8f0f5e feat(settings): toggle for the symbol-picker chat-input button
Adds a Configuration property, defaulted to enabled, and a checkbox in
the Chat settings tab's Behaviour section. Strings live in HellionStrings
so DE/EN stays in sync. Defaults aligned with our 'discoverable by
default, hidden by user choice' convention. Schema stays at v17 — the
new boolean is additive, the default constructor covers existing configs.
2026-05-16 10:05:37 +02:00
JonKazama-Hellion 0e470fcdce feat(ui): SymbolPicker BMP tab and session-only recents
Second tab exposes the server-verified BMP whitelist (round-tripped via
/echo and /say in the v1.4.10 preflight). Recent-used row at the top
floats the user's last sixteen picks across both tabs, move-to-front
on reuse. Recents stay session-only by design — no Configuration touch,
schema unchanged.
2026-05-16 09:27:58 +02:00
JonKazama-Hellion abbbf95002 feat(ui): add SymbolPicker popup with FFXIV icon tab
New popup attached to the chat input lets the user browse and insert
Dalamud SeIconChar glyphs (161 PUA codepoints, server-safe by design).
Search field filters by enum name. Multi-insert keeps the popup open
until the user clicks elsewhere. BMP tab follows in the next commit.
2026-05-16 01:11:12 +02:00
JonKazama-Hellion fbbbeebade refactor(commands): cache slash-command wrappers in private fields
TearDownCommands attached the same instance via re-Register with identical
args, which was functionally a no-op but masked a latent bug if Description
or ShowInHelp ever diverged between Setup and Teardown. Hold the wrapper
instances as nullable fields so Teardown can detach the live registration
directly. Mirrors the cached-wrapper pattern in ChatLogWindow.
2026-05-15 20:18:41 +02:00
JonKazama-Hellion 7c9b90c767 Merge feature/v1.4.9 — Plugin-Load Render Polish
Security / scan (push) Successful in 22s
Build / Build (Release) (push) Successful in 31s
Forge Announce / Post changelog to Hellion Forge (push) Successful in 7s
Release / Build and attach release ZIP (push) Successful in 36s
2026-05-15 13:35:37 +02:00
JonKazama-Hellion b81894b859 docs: surface the ChatTwo IPC compatibility layer in v1.4.9 patch notes
Adds a "ChatTwo IPC compatibility layer" bullet across all six
release-note surfaces so the new behaviour from commits 8c4afaa and
655c903 is visible to users via the manifest installer, the Gitea
release page, the README project-status section, the changelog/roadmap
docs and the Forge-Discord announcement.

Files touched:
- HellionChat/HellionChat.yaml: bullet added inside the v1.4.9
  changelog block, preserved order so the regression-tripwire line
  still comes before the migration-stays line.
- repo.json: Changelog field kept synchronous (JSON-escaped newlines).
- README.md: project-status paragraph extended with a one-sentence
  recap of the IPC mirror and the conflict-detection caveat.
- docs/CHANGELOG.md: bullet inserted between the profiling-logs and
  migration-stays bullets, code-fenced gate names.
- docs/ROADMAP.md: v1.4.9-released section gets the same recap so the
  cycle history stays self-describing.
- .github/forge-posts/v1.4.9.md: German-only bullet for the Discord
  embed, slotted before the migration-v17 bullet. Char-cap holds —
  preflight Block C reports the embed total well under 5500 chars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:04:18 +02:00
JonKazama-Hellion 655c903cb5 feat(ipc): mirror context-menu IPC gates under ChatTwo namespace (v1.4.9 R4 ext)
Extends commit 8c4afaa: the TypingIpc mirror covered only two of the six
ChatTwo IPC slots. Third-party plugins like Artisan and AllaganTools
subscribe to a different ChatTwo IPC surface — the context-menu
integration (ChatTwo.Register / Unregister / Available / Invoke) that
lets them push item-links into the chat. Smoke test against the
deployed v1.4.9 build showed Artisan logging "Chat2 is not available"
because those four gates were not yet mirrored.

This commit adds the missing four ChatTwo-prefixed provider gates in
IpcManager.cs:

- ChatTwo.Register  (Func<string>) — bound to the existing Register()
  backing method, so plugins that subscribe via either namespace land
  in the same Registered list.
- ChatTwo.Unregister (Action<string>) — bound to the existing
  Unregister() backing method, same shared-state rationale.
- ChatTwo.Available (Action<>) — SendMessage() fires from the ctor right
  after AvailableGate.SendMessage(), so any subscriber waiting on the
  "Chat 2 became available" signal sees both events.
- ChatTwo.Invoke (Action<string, PlayerPayload?, ulong, Payload?,
  SeString?, SeString?>) — Invoke() fans the context-menu event out to
  both InvokeGate and ChatTwoInvokeGate in lockstep. Subscribers compare
  on the registration ID they got back from Register, so the
  shared-backing approach keeps that contract intact regardless of which
  namespace they subscribed under.

Dispose() unregisters all four ChatTwo gates plus the four existing
HellionChat gates. The conflict-detection that prevents ChatTwo from
loading alongside HellionChat guarantees no slot collision at runtime.

With this commit the full ChatTwo IPC surface (6 of 6 slots) is mirrored:
- ChatTwo.GetChatInputState     (TypingIpc, commit 8c4afaa)
- ChatTwo.ChatInputStateChanged (TypingIpc, commit 8c4afaa)
- ChatTwo.Register              (IpcManager, this commit)
- ChatTwo.Unregister            (IpcManager, this commit)
- ChatTwo.Available             (IpcManager, this commit)
- ChatTwo.Invoke                (IpcManager, this commit)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:01:12 +02:00
JonKazama-Hellion 8c4afaac17 feat(ipc): mirror TypingIpc provider slots under ChatTwo namespace (v1.4.9 R4)
HellionChat replaces ChatTwo (conflict detection prevents parallel loading)
but third-party plugins with a no-fork policy keep subscribing only to the
ChatTwo.*-prefixed IPC gates. Mirroring the two TypingIpc provider slots
under the ChatTwo namespace lets those plugins keep working without code
changes on their side.

Mirrored slots:
- ChatTwo.GetChatInputState  ←→ HellionChat.GetChatInputState
- ChatTwo.ChatInputStateChanged ←→ HellionChat.ChatInputStateChanged

Implementation:
- Two additional ICallGateProvider fields (ChatTwoStateQueryGate +
  ChatTwoStateChangedGate) with the identical ChatInputState tuple
  signature. The tuple's underlying types match ChatTwo's surface byte-
  for-byte (bool/bool/bool/bool/int/ushort — ChatType is `ushort` in both
  repos), so Dalamud's IPC marshalling matches across plugin boundaries
  even when the subscribing plugin defines its own copy of the ChatType
  enum.
- ctor registers the new provider gates and binds RegisterFunc(GetState)
  to ChatTwoStateQueryGate so query calls route to the same backing path.
- Update() pushes the state to both ChatTwoStateChangedGate and the
  existing StateChangedGate in lockstep.
- Dispose() unregisters both query gates.

Ipc/ExtraChat.cs is intentionally unchanged — it is a subscriber on
ExtraChat's own IPC, not a provider, so no compatibility mirror applies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:47:06 +02:00
JonKazama-Hellion c6a3780753 docs: add v1.4.9 changelog and forge announcement post
Synchronises the v1.4.9 changelog across the manifest sources that the
Dalamud plugin installer, the gitea repo.json feed and the Forge auto-
announce workflow read at release-tag time.

Files touched:
- HellionChat/HellionChat.yaml: v1.4.9 block inserted at the top of the
  changelog: literal. v1.4.5 dropped to keep the slim-rule at 4 subblocks
  (preflight Block C enforces YAML_VERSIONS <= 4). Current set is
  v1.4.9/v1.4.8/v1.4.7/v1.4.6.
- repo.json: Changelog field kept synchronous with the yaml — v1.4.9
  block prepended, v1.4.5 substring removed, JSON-escaped newlines.
- .github/forge-posts/v1.4.9.md: new file with frontmatter (subtitle
  "Plugin-Load Render Polish", versionsnatur "Performance-Patch") and
  a German-only body. The English half of the eventual Discord embed
  is pulled automatically from the yaml changelog at tag-push time by
  .gitea/workflows/forge-announce.yml — same workflow as v1.4.4
  onwards, the post file does not carry an English block.

Char-cap pre-check passes (title 46 + description ~2700 + footer 33 =
~2800 chars, well under the 5500-char Discord embed total cap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:51:25 +02:00
JonKazama-Hellion d9f6704316 chore: bump version to 1.4.9, sync manifest
Manifest version bump for the v1.4.9 release cut. Schema-required v16
stays unchanged (R1/R2/R3 are all config-neutral refactors).

Files touched:
- HellionChat/HellionChat.csproj: <Version> 1.4.8 -> 1.4.9
- HellionChat/Plugin.cs: schema-migration error string self-reference
  (v1.4.8 -> v1.4.9, required schema v16 stays)
- repo.json: AssemblyVersion, TestingAssemblyVersion, 3x DownloadLink*
  URLs all bumped to 1.4.9 / v1.4.9. Changelog field is still on v1.4.8;
  the v1.4.9 block plus v1.4.5 slim-drop land in the next commit.
- README.md: shield badge, version header in lead paragraph, project-
  status block rewritten for v1.4.9 (Plugin-Load Render Polish).
- docs/CHANGELOG.md: v1.4.9 block inserted above v1.4.8.
- docs/ROADMAP.md: v1.4.9 moved into the released-versions list,
  "Next Cycle" header now targets v1.4.10 (Render Clipper + Symbol
  Picker reserves carried over from the v1.4.9 plan).

yaml changelog block and repo.json Changelog field follow in the
docs commit so the slim-drop of v1.4.5 stays atomic with the v1.4.9
block insert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:31:35 +02:00
JonKazama-Hellion 011490368b perf(draw): defer non-essential first-frame rendering (v1.4.9 R2)
Cut first-frame HITCH from ~127ms median down to ~76ms median (4-reload
sample, threshold lowered to 1ms for measurement) — comfortably under
Dalamud's 100ms warning threshold. ChatTwo upstream sits at ~63ms median
for comparison; the remaining ~13ms gap is the cost of HellionChat-only
features (Sidebar tab view, custom StatusBar, Honorific integration).

Mechanism: a single `_firstFrameDone` flag (flipped in Draw's finally
block) gates six sections that don't need to render on frame 0:

  - StatusBar.Draw (~12ms): the bottom status bar
  - DrawChannelName chunks (~17ms): SeString-Renderer layout, replaced
    with a plain-text fallback (activeTab.Name) for frame 0
  - PositionReset/BoundsCheck (~10ms): EnsureWindowOnScreen viewport
    iteration, only matters once the user notices a mispositioned window
  - DrawV061HintBannerIfNeeded (~3-5ms): v0.6.1 migration notice
  - DrawAutoComplete (~6ms): renders nothing until the user types a command
  - InputPreview.CalculatePreview (~3-5ms): triggers InputPreview first-
    frame lazy init, user-typing-driven anyway

Frame 1 then renders all of them in ~40ms (still well under the warning
threshold), and frames 2+ stay at 0ms as before. User sees the deferred
sections ~17ms (60fps) later than before — invisible inside the ~2.5s
Atlas-Build window after every plugin reload.

Hypothesis triage from the R2-profiling pass:
  - (a) Atlas-Sync-Fallback: falsified. xllog shows the Atlas-Complete
    line always lands ~2.5s before the HITCH frame.
  - (b) Theme-Apply ABGR-Cache-Init: not dominant. PushGlobal is 5ms.
  - (c) Multiple-Window-Render: falsified in v1.4.9 Stage-2-Lazy-Init
    diagnose (deferred 4 windows, no measurable delta).
  - (d) DrawList-Setup-Cost per Window: actual root cause. Layout cost
    distributes evenly across ~10 ImGui sections inside ChatLogWindow
    (5-20ms each). No single hot-spot to optimise — the six selective
    skips above are the pragmatic fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:14:13 +02:00
JonKazama-Hellion 8ed10a536b refactor(plugin): centralise slash-command registration for lazy-window readiness (v1.4.9 R1 stage 1)
Pull the four user-triggered slash-commands (/hellion, /hellionView,
/hellionDebugger, /hellionSeString) plus the two Plugin-Manager
UiBuilder hooks (OpenConfigUi, OpenMainUi) out of their window
constructors and into a central Plugin.SetupCommands method so they
work before their target window has been opened the first time. A
matching TearDownCommands runs as the first CaptureFailure inside the
framework-thread teardown lambda. /hellion and /hellionSeString stay
under the same #if DEBUG guard SeStringDebugger had before. The four
window classes keep their public Dispose method signatures so the
existing Plugin.DisposeAsync method-group binding still resolves —
the bodies are now empty pointers to TearDownCommands. The pre-v1.4.9
`OpenMainUi` body that flipped SettingsWindow.IsOpen and the three
private Toggle(string, string) method-group wrappers are gone since
the central handlers call SettingsWindow.Toggle() / DbViewer.Toggle()
etc. directly.

The properties stay eager in stage 1 — the lazy-init switch lands in
stage 2 with the matching `_lazyWindowLock` guard around AddWindow
and RemoveAllWindows. Doing it in two commits keeps the slash-command
correctness verifiable on its own.

Smoke (release build): /hellion, /hellionView, /hellionDebugger,
/clearhellion plus Plugin-Manager Settings and Open buttons all
toggle their target window. /hellionSeString remains DEBUG-only as
before.
2026-05-15 00:28:18 +02:00
JonKazama-Hellion 6051e49307 chore(profiling): instrument plugin-load hot paths (v1.4.9 R3)
Bump AutoTranslate-warmup and FilterAllTabs log-level from Debug to
Information so the xllog tail surfaces them without a Debug filter.
Wrap MessageStore.Connect and MessageStore.Migrate in Stopwatches so
the SQLite open and migration-chain costs are visible too.

Sub-Task 3.4 Befund on v1.4.8-baseline (4 reloads, medians):
- MessageStore.Connect: 50.5 ms
- MessageStore.Migrate:    2 ms
- MessageManager.FilterAllTabs: 68.5 ms
- AutoTranslate warmup:  108 ms
- UiBuilder HITCH:       108.9 ms

Outcome D — none of the three dominates the 200 ms threshold. The
ChatTwo "300 ms" comment for AutoTranslate is falsified at ~108 ms;
SQLite is not the bottleneck (52.5 ms total); FilterAllTabs runs on
the worker thread and only competes for CPU slots. The HITCH is left
unexplained by these probes, which keeps Hypothesis c (multi-window
WindowSystem.Draw initial pass) as the main R2 suspect to be
validated by the R1 lazy-window refactor.

Logs stay in as belt-and-suspenders for future plugin-load
regressions.
2026-05-14 23:33:56 +02:00
JonKazama-Hellion 55120e6572 Merge branch 'feature/v1.4.8'
Security / scan (push) Successful in 23s
Build / Build (Release) (push) Successful in 29s
Forge Announce / Post changelog to Hellion Forge (push) Successful in 7s
Release / Build and attach release ZIP (push) Successful in 37s
2026-05-14 12:12:06 +02:00
JonKazama-Hellion 7542d48983 perf(dbviewer): dispatch FTS filter to worker thread
FullTextSearch + LoadByGuids could stall the draw thread for 100-300 ms
on large databases with a popular search term. The two hot trigger sites
(FTS toggle, search input) now route via TriggerFilterRefresh, which
dispatches the FTS path to Task.Run; the in-memory page-filter path
stays inline because it is sub-ms on the loaded page array.

_ftsFilterSeq is bumped per trigger so a late worker recognises itself
as stale and drops its result instead of overwriting a newer one. The
date/channel and history workers already lived on Task.Run and are
untouched.

Surfaced during the v1.4.8 pre-tag review.
2026-05-14 11:48:40 +02:00
JonKazama-Hellion 7b36763359 docs: add v1.4.8 changelog and forge announcement post
- HellionChat.yaml: v1.4.8 changelog block above v1.4.7, v1.4.4
  dropped per slim-rule (verify-changelog-sync enforces max 4).
- repo.json: Changelog field synchronised with yaml, same slim-drop.
- .github/forge-posts/v1.4.8.md: bilingual announcement post (DE
  body, EN block resolved from yaml at workflow time). Frontmatter
  subtitle 32/60 chars, versionsnatur 12/40 chars, embed total
  ~2787/5500 chars.
2026-05-14 10:51:50 +02:00
JonKazama-Hellion eecedd9f97 chore: bump version to 1.4.8, sync manifest
- csproj <Version>, Plugin.cs schema-gate self-reference, repo.json
  (AssemblyVersion, TestingAssemblyVersion, 3x DownloadLink URLs).
- README.md shield badge, version header, Project Status body.
- docs/CHANGELOG.md gains a v1.4.8 section above v1.4.7.
- docs/ROADMAP.md flips Next Cycle to v1.4.9 (Plugin-Load Render
  Polish), v1.4.8 moves into the released history above v1.4.7.
- Config schema stays at v17, Migration v17 stays additive.

repo.json Changelog field and HellionChat.yaml changelog block plus
the new forge-posts/v1.4.8.md follow in a separate commit (slim-drop
of v1.4.4 happens there).
2026-05-14 10:28:51 +02:00
JonKazama-Hellion 1003a88cad fix(messagestore): match TEXT-stored UUID form in FTS bulk insert and LoadByGuids
messages.Id is declared BLOB but stored as TEXT because Microsoft.Data.Sqlite
binds Guid parameters as UUID strings (UpsertMessage uses AddWithValue with
a Guid). RebuildFtsIndex cast reader.GetValue(0) to byte[] and threw
InvalidCastException at the first row. LoadByGuids bound byte[] params
against the TEXT-stored Id and would have returned no rows once the index
had built.

- RebuildFtsIndex reads via GetGuid and stores ToString() in
  messages_fts.message_guid.
- LoadByGuids parses incoming UUID strings and binds them as Guid so
  Microsoft.Data.Sqlite re-serialises to TEXT, matching the messages.Id
  storage form.
- DbViewer caller variable renamed hexIds -> guidHits for clarity.
2026-05-14 09:58:58 +02:00
JonKazama-Hellion 299fd59cbb refactor(retention): use Framework.RunOnTick instead of synchronous .Wait()
Retention sweep no longer blocks for ~194ms on Framework.Run().Wait().
The clear+refilter pair is now scheduled on the next framework tick, so
it still runs on the framework thread (keeping the Tabs-list mutation
serialisation invariant -- Plugin.Config.Tabs is plain List<Tab> and
AutoTellTabsService can mutate it from background paths) but does not
block the sweep thread while the framework finishes the current frame.

A new _isDisposing volatile bool is set as the first statement in
DisposeAsync so a deferred tick that fires after teardown bails before
it touches MessageManager / Log / static fields the dispose path has
already cleared. The retention worker is IsBackground=true so plugin
unload can race against a still-pending tick.

The existing RetentionSweepLock / RetentionSweepRunning serialisation
covers the not-two-sweeps-at-once invariant; we don't add a CTS here
because RunOnTick is fire-and-forget and the framework service owns
the tick lifecycle.

v1.4.8 B3. Coverage via in-game smoke (frame-time trace during a
retention sweep run) in Task 9 -- no Build-Suite test because the
suite has no FakeFramework fixture and the change is a schedule-form
swap rather than new behaviour.
2026-05-14 00:00:53 +02:00
JonKazama-Hellion 74bcb91b65 feat(themes): auto-reload active custom theme on disk change
When the user edits their active custom theme JSON in an external editor
and saves, the change now propagates to HellionChat within ~1 second
without re-selecting the theme in the picker.

RefreshActiveIfStale runs from Plugin.Draw on every frame but the actual
File.GetLastWriteTimeUtc stat is 1Hz-throttled -- 60fps would otherwise
mean 3600 stats/min, more on Wine. Built-in themes short-circuit on the
IsBuiltIn check; custom themes without a captured source path (Switch
fell to default) short-circuit on the null check.

Switch() now captures the source path of custom themes via an out-param
on LoadCustomBySlug, which now reverse-looks-up against the existing
_customCache (no re-parse, no extra disk IO). Plugin.LoadAsync warms the
cache via AllCustom() once before the first Switch so a Config.Theme
pointing at a custom slug does not fall through to the built-in default
on a cold registry.

Switch's lookup order is now built-in-first to match Get(slug), so a
user-authored JSON that declares a built-in slug is consistently
ignored in both code paths.

Pure-helper ThemeStampDiff isolates the stamp-diff rules for the
Build-Suite (covers DateTime.MinValue hold-the-line semantics).

v1.4.8 B2.
2026-05-13 23:22:14 +02:00
JonKazama-Hellion 2c64aaa251 fix(statusbar): make height DPI-aware via GetTextLineHeightWithSpacing
Replace the fixed 22px const Height with a computed property that bakes
in the ImGui font line height plus a GlobalScale-rounded 2px spacer.
The constant clipped the bottom bar on Windows display-scaling >100%
because ImGui rendered the actual font taller than 22px; the bar then
got pushed off the window edge.

ChatLogWindow.cs:423 reservation drops the explicit +2 because the
spacer now lives inside Height. Same idiom as the v1.4.6 F7.2 underline
pill in ChatLogWindow.cs:1639-1653.

v1.4.8 B1. Coverage via in-game smoke on Windows (Jin) and Linux/Wayland
in Task 9 -- DrawList-coupled, no Build-Suite test.
2026-05-13 22:42:40 +02:00
JonKazama-Hellion 607d2c7241 feat(dbviewer): full-text-search toggle wired to FTS5 query API
New UseFullTextSearch transient UI bool flips DbViewer.Filter() between
the existing local page filter (default) and the FTS5 MATCH path across
the whole database. ImRaii.Disabled blocks the toggle while the bulk-insert
worker is still building the index; the HelpMarker swaps between two
hints, one for the indexing state and one for the phrase-match advisory
once the index is ready.

Three new HellionStrings entries cover EN + DE + the Designer accessor:
- DbViewer_FullTextToggle (label)
- DbViewer_FullTextToggle_Hint_Indexing (tooltip while indexing)
- DbViewer_FullTextToggle_Hint_PhraseMode (tooltip once ready, warns
  multi-word terms match as phrases and how to opt into raw MATCH syntax)

Filter() short-circuits to the local fallback if the toggle is on but
ftsReady has flipped back to false -- defensive against a mid-session
Dispose-and-reopen during indexing.

v1.4.8 H2 Sub-Task 4.4.
2026-05-13 22:08:32 +02:00
JonKazama-Hellion b2a0f3a77c feat(messagestore): add FullTextSearch + LoadByGuids with MATCH-syntax escape
Two new public query methods plus an internal EscapeFtsTerm helper:
- FullTextSearch(term, limit) runs MATCH against messages_fts and returns
  hex-encoded GUIDs sorted by FTS5 rank. Empty/whitespace short-circuits
  to an empty list so callers can fall back to the local page filter.
- LoadByGuids(hexIds) resolves the hex GUIDs back to Message rows via
  WHERE Id IN (...). Chunked at 500 to stay below SQLite's 999-parameter
  cap, and the BLOB-PK autoindex means the join is O(log n) per id.
- EscapeFtsTerm wraps user input in double-quotes so multi-word queries
  match as a phrase, not as per-word AND. Users opt into raw MATCH
  syntax by writing their own quotes.

Plus _readLock serialises every Connection-touching internal method
(UpsertMessage, MessageCount, all readers, retention writers, etc.).
The DbViewer filter worker now runs FullTextSearch on a Task.Run thread
while the PendingMessageThread keeps calling UpsertMessage; SqliteConnection
is not safe for concurrent use, so this single lock is the minimal
architecture change that closes the race. The Lazy-Enumerator methods
(StreamForExport, GetDateRange, GetPagedDateRange) hold the lock only
through command-setup + ExecuteReader; v1.4.8 doc-notes the caveat for
the v1.5.x DI cycle to address with a snapshot-to-list or connection pool.

RebuildFtsIndex stays outside the lock -- it owns its own SqliteConnection
via OpenSecondaryConnection.
2026-05-13 21:27:17 +02:00
JonKazama-Hellion d26c4701fa feat(messagestore): async background FTS5 bulk-insert with progress notification
Adds the worker that fills the messages_fts virtual table after Migrate4.
The bulk-insert runs off the framework thread on its own SqliteConnection
opened via OpenSecondaryConnection -- WAL lets the live UpsertMessage
path on the primary Connection keep flowing, and the worker's writer
lock yields every 500 rows with a 5ms breather so PendingMessageThread
does not hit "database is locked" after DefaultTimeout=5s.

InitFtsReadyCache runs in the ctor and short-circuits to ready=true when
the index is already populated or when the messages table is empty. The
DbViewer (Task 4.4) reads IsFtsIndexBuilt per frame as a single volatile
field read.

Plugin.cs LoadAsync kicks the worker after FilterAllTabsAsync, gated on
IsFtsIndexBuilt and a CancellationTokenSource that DisposeAsync cancels
before MessageManager tears down. Progress reports back via IActiveNotification,
marshalled onto the framework thread via Framework.RunOnTick. Success path
finishes the notification as Success with a 5s linger; cancellation
dismisses it; an error swaps the type to Error with a fallback hint.
2026-05-13 20:38:05 +02:00
JonKazama-Hellion 7f317a2b18 refactor(messagestore): extract BuildConnectionString and ApplyPragmas helpers
Pre-step for the v1.4.8 FTS5 bulk-insert worker. The worker opens its
own secondary SqliteConnection on the same db path so the WAL journal
lets parallel reads/writes through, and it has to apply the exact same
connection-string options and PRAGMAs as Connect() -- otherwise the
worker connection drifts the moment Connect grows a new pragma.

Splitting BuildConnectionString + ApplyPragmas out lets both Connect()
and the upcoming OpenSecondaryConnection() share the same source of
truth instead of duplicating the body. No behaviour change.
2026-05-13 20:31:43 +02:00
JonKazama-Hellion 38149059c3 feat(messagestore): add Migrate4 with standalone FTS5 virtual table
Lays down a messages_fts virtual table with message_guid (UNINDEXED, hex
TEXT of the BLOB primary key), sender_text and content_text columns
using the unicode61 tokenizer with diacritic folding. Standalone FTS5
without content='messages' linking, because messages.Id is BLOB and
FTS5's content_rowid contract requires an INTEGER rowid alias.

LoadByGuids (Task 4.3) will resolve the hex GUIDs back to messages rows
via WHERE Id IN (...) joins. Schema step only -- the bulk-insert worker
that fills the index lives in Task 4.2.

Internal Connection property exposure plus a HasMessagesFtsTable helper
let the Build-Suite verify Migrate4 without raw PRAGMA glue in each test.

v1.4.8 H2 Sub-Task 4.1.
2026-05-13 19:51:54 +02:00
JonKazama-Hellion 67175419a9 refactor(messagestore): extract ReadMessageRow as shared deserialiser
Pure deserialisation helper that pulls one row from the current reader
position into a Message. The MessageEnumerator load path delegates to
it, and the upcoming FTS-join LoadByGuids (Task 4.3) will share the
same code so both stay in lockstep when the column layout shifts.

Pre-step for v1.4.8 H2 FTS5 full-text search.
2026-05-13 19:20:55 +02:00
29 changed files with 2056 additions and 444 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.
+21
View File
@@ -0,0 +1,21 @@
---
subtitle: Hook-Layer und Polish-Quick-Wins
versionsnatur: Polish-Patch
---
- DbViewer Volltext-Suche: optionaler FTS5-Index über die ganze Chat-Historie.
Wird beim ersten v1.4.8-Start asynchron im Hintergrund gebaut, Progress als
Toast. Lokale Page-Suche bleibt Default. Such-Eingaben werden als exakte
Wortfolge gematcht; mehrere Wörter werden nur gefunden, wenn sie zusammen
und in der Reihenfolge stehen. Wer rohe FTS5-MATCH-Syntax nutzen will, setzt
eigene Anführungszeichen um den Suchbegriff.
- Custom-Theme-Files laden sich beim Speichern automatisch neu, wenn das Theme
aktiv ist. Kein Picker-Klick mehr nötig.
- Retention-Sweep blockt nicht mehr den Framework-Thread. Der Mini-Hitch von
~194ms pro Sweep ist weg.
- Statusleiste rendert sauber bei Windows-Skalierung über 100%.
- Receive-Suppressed-Tells-Routing wurde in diesem Cycle untersucht und auf
v1.5.x verschoben: wenn andere Plugins Tells via CheckMessageHandled
unterdrücken, überspringt FFXIVs Chat-Pipeline den RaptureLogModule-Resolver
und HellionChats Tab-Routing verliert den Tell-Partner. Der Fix liegt
architektonisch neben dem geplanten Ad-Block-Hook-Layer und kommt dort mit.
+37
View File
@@ -0,0 +1,37 @@
---
subtitle: Plugin-Load Render Polish
versionsnatur: Performance-Patch
---
- First-Frame-HITCH unter 100 ms: der erste Render-Frame des Plugins liegt
jetzt bei ~76 ms Median (vorher ~127 ms), die Dalamud-Warnung
„UiBuilder(Hellion Chat) > 100ms" beim Plugin-Start ist damit weg.
Erreicht durch das Verlagern von sechs nicht-essentiellen Render-
Sektionen (Statusleiste, Kanalname-Chunks, Fenster-Bounds-Check,
Hinweis-Banner, Autocomplete, Input-Preview) auf den zweiten Frame.
Bei 60 fps sieht man die deferred-Sektionen ~17 ms später, was im
Atlas-Build-Fenster nach einem Reload unsichtbar bleibt.
- Slash-Commands zentral registriert: /hellion, /hellionView,
/hellionSeString und /hellionDebugger werden jetzt im Plugin-Load zentral
registriert statt erst beim ersten Öffnen ihres Ziel-Fensters. Heißt: die
Befehle funktionieren ab dem ersten Tick, auch wenn das jeweilige Fenster
nie geöffnet wurde. Der „Einstellungen"-Button im Plugin-Manager hängt am
selben Pfad.
- Plugin-Load-Diagnose-Logs als Tripwire: die Profiling-Logs für
MessageStore.Connect, MessageStore.Migrate, FilterAllTabs und den
Auto-Translate-Warmup bleiben auf Information-Level eingeschaltet. Falls
eine zukünftige Änderung die Lade-Zeit wieder über 100 ms drückt, taucht
der Mehrverbrauch direkt im /xllog auf, ohne dass jemand erst den
Debug-Filter einschalten muss.
- ChatTwo-IPC-Kompatibilitäts-Layer: HellionChat spiegelt jetzt die
komplette ChatTwo-IPC-Surface (`GetChatInputState`,
`ChatInputStateChanged`, `Register`, `Unregister`, `Available`,
`Invoke`) zusätzlich zu unseren eigenen `HellionChat.*`-Gates unter
dem `ChatTwo.*`-Namensraum. Drittseitige Integrationen die nur auf
ChatTwo's IPC reagieren, etwa die Kontextmenü-Hooks von Artisan und
AllaganTools, funktionieren damit weiter ohne Code-Änderung auf
ihrer Seite. Die Conflict-Detection blockiert das parallele Laden
von ChatTwo, daher kein Namensraum-Konflikt im Live-Betrieb.
- Migration v17 unverändert: kein Schema-Bump, kein Config-Migrations-
Aufwand. Nach dem Update läuft das Plugin gegen die bestehende
v17-Datenbank weiter.
+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.7</Version>
<Version>1.4.10</Version>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Use lock file to pin exact versions -->
+116 -82
View File
@@ -35,6 +35,122 @@ 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
render cost drops from ~127 ms median to ~76 ms median,
comfortably under Dalamud's 100 ms HITCH warning threshold.
- First-frame defer: six non-essential rendering sections inside
ChatLogWindow skip their first Draw and run one frame later
(bottom status bar, channel-name SeString chunks, window bounds
check, v0.6.1 hint banner, autocomplete, input-preview
calculation). User-visible delay is ~17 ms at 60 fps, hidden
inside the post-reload font-atlas build window.
- Slash-command centralisation: /hellion, /hellionView,
/hellionSeString and /hellionDebugger are registered in
LoadAsync instead of inside the corresponding window
constructors. The plugin-manager Open and configuration buttons
hang on the same path.
- Plugin-load profiling logs stay on at Information level
(MessageStore connect/migrate, FilterAllTabs, auto-translate
warmup) as a regression tripwire — a future load past 100 ms
will show up in /xllog without a Debug filter.
- 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. Third-party
integrations that historically only subscribe to ChatTwo's
IPC — for example Artisan's and AllaganTools' context-menu
hooks — 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).
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
**v1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)**
Ninth sub-patch of the v1.4.x polish-sweep series. Hook-layer
cluster (DbViewer FTS5 full-text search, ad-block foundation
investigation) plus three polish quick-wins.
- DbViewer full-text search: optional FTS5 index across the full
chat history. Built asynchronously on first load after the
update with a progress toast. The local page-filter remains
available as the default mode. Queries match as exact phrases
-- multi-word terms must appear together in order; advanced
users can opt into raw FTS5 MATCH syntax by wrapping their own
double-quotes.
- Custom theme files now auto-reload when edited while the theme
is active -- no need to re-click the theme in the picker.
- Retention sweep no longer blocks the framework thread, removing
the ~194ms mini-hitch per sweep.
- Status bar renders correctly at Windows display scaling > 100%.
- Receive-suppressed-tells routing investigated this cycle and
postponed to v1.5.x: when other plugins suppress tells via
CheckMessageHandled, the FFXIV chat pipeline skips the
RaptureLogModule.AddMsgSourceEntry path so HellionChat's
ContentIdResolverHook does not fire and tell-partner
identification breaks. The fix belongs next to the planned
ad-block hook layer where the same patch surface comes up.
- Internal: messages.Id is declared BLOB but stored as TEXT
(Microsoft.Data.Sqlite Guid binding). FTS bulk insert and
LoadByGuids match the TEXT storage form on both sides.
Migration v17 stays (no schema bump).
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
**v1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)**
Eighth sub-patch of the v1.4.x polish-sweep series. First
@@ -78,86 +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).
---
**v1.4.5 — UX and Robustness (2026-05-12)**
Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw
failures surface as a notification, the first-run wizard has an
explicit "Later" option, the input history clears on plugin reload,
and the status bar version slot stops clipping in narrow windows.
- Chat window draw errors now show a one-shot notification instead
of failing silently — stack trace stays in /xllog
- First-run wizard: explicit "Later — keep defaults" button.
Closing the X no longer silently accepts the defaults; the wizard
reopens on the next plugin load if nothing was picked
- InputHistoryService clears on plugin dispose so the previous
session's typed commands don't bleed into the next load
- Status bar hides the version slot when the chat window is too
narrow to fit all five slots without overlap
- Internal: explicit session-only Auto-Tell-Tab invariant in
Plugin.cs plus a pinning test in the Build-Suite
- Internal: FontManager falls back to the system font if the
embedded Hellion font resource is missing — logs a Warning
---
**v1.4.4 — Threading and IPC safety polish (2026-05-12)**
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
assumptions are documented per-method, a hot-path lock falls
away, and the privacy filter speaks up when an unknown ChatType
shows up.
- AutoTellTabs hot-path getter uses an Interlocked counter
instead of taking the lock on every read
- Honorific integration: per-method threading banners, plus
Warning-level log on unsubscribe failure
- AutoTranslate warmup thread marked IsBackground so plugin
unload doesn't wait for it
- PrivacyFilter logs once per unknown ChatType so a future
patch's added channel doesn't drop off the radar
- New installs persist unknown channels by default; existing
configs keep their explicit choice
---
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
+23
View File
@@ -19,6 +19,17 @@ internal sealed class TypingIpc : IDisposable
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
private ICallGateProvider<ChatInputState, object?> StateChangedGate { get; }
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Some third-party plugins
// have a no-fork policy and subscribe only to ChatTwo.*-prefixed IPC
// gates. HellionChat replaces ChatTwo (conflict detection prevents
// parallel loading), so mirroring the ChatTwo provider slots lets those
// plugins keep working without code changes on their side. The tuple
// shape is textually identical to ChatTwo's IPC surface (same member
// order, same underlying types — ChatType is `ushort` in both repos)
// so Dalamud's IPC marshalling matches across plugin boundaries.
private ICallGateProvider<ChatInputState> ChatTwoStateQueryGate { get; }
private ICallGateProvider<ChatInputState, object?> ChatTwoStateChangedGate { get; }
private ChatInputState LastState;
private bool HasState;
@@ -33,7 +44,16 @@ internal sealed class TypingIpc : IDisposable
"HellionChat.ChatInputStateChanged"
);
// v1.4.9 R4: ChatTwo-prefixed compatibility slots (see class-level comment).
ChatTwoStateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
"ChatTwo.GetChatInputState"
);
ChatTwoStateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>(
"ChatTwo.ChatInputStateChanged"
);
StateQueryGate.RegisterFunc(GetState);
ChatTwoStateQueryGate.RegisterFunc(GetState);
}
private ChatInputState BuildState()
@@ -67,10 +87,13 @@ internal sealed class TypingIpc : IDisposable
HasState = true;
LastState = state;
StateChangedGate.SendMessage(state);
// v1.4.9 R4: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
ChatTwoStateChangedGate.SendMessage(state);
}
public void Dispose()
{
StateQueryGate.UnregisterFunc();
ChatTwoStateQueryGate.UnregisterFunc();
}
}
+49
View File
@@ -19,6 +19,26 @@ internal sealed class IpcManager : IDisposable
object?
> InvokeGate { get; }
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Third-party plugins with
// a no-fork policy (e.g. Artisan, AllaganTools) only subscribe to the
// ChatTwo.*-prefixed context-menu integration gates. Mirroring all four
// provider slots under the ChatTwo namespace lets those plugins keep
// working without code changes on their side. Conflict detection
// prevents ChatTwo and HellionChat from loading in parallel, so no slot
// collision risk.
private ICallGateProvider<string> ChatTwoRegisterGate { get; }
private ICallGateProvider<string, object?> ChatTwoUnregisterGate { get; }
private ICallGateProvider<object?> ChatTwoAvailableGate { get; }
private ICallGateProvider<
string,
PlayerPayload?,
ulong,
Payload?,
SeString?,
SeString?,
object?
> ChatTwoInvokeGate { get; }
internal List<string> Registered { get; } = [];
public IpcManager()
@@ -41,7 +61,32 @@ internal sealed class IpcManager : IDisposable
object?
>("HellionChat.Invoke");
// v1.4.9 R4: ChatTwo-prefixed mirrors of the four context-menu slots
// above. Share the same Register/Unregister backing methods so a
// plugin that subscribes via either namespace lands in the same
// Registered list. SendMessage on Invoke fans out to both gates.
ChatTwoRegisterGate = Plugin.Interface.GetIpcProvider<string>("ChatTwo.Register");
ChatTwoRegisterGate.RegisterFunc(Register);
ChatTwoAvailableGate = Plugin.Interface.GetIpcProvider<object?>("ChatTwo.Available");
ChatTwoUnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>(
"ChatTwo.Unregister"
);
ChatTwoUnregisterGate.RegisterAction(Unregister);
ChatTwoInvokeGate = Plugin.Interface.GetIpcProvider<
string,
PlayerPayload?,
ulong,
Payload?,
SeString?,
SeString?,
object?
>("ChatTwo.Invoke");
AvailableGate.SendMessage();
ChatTwoAvailableGate.SendMessage();
}
internal void Invoke(
@@ -54,6 +99,8 @@ internal sealed class IpcManager : IDisposable
)
{
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
// v1.4.9 R4: fan out the same event to plugins listening on ChatTwo.Invoke.
ChatTwoInvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
}
private string Register()
@@ -72,6 +119,8 @@ internal sealed class IpcManager : IDisposable
{
UnregisterGate.UnregisterAction();
RegisterGate.UnregisterFunc();
ChatTwoUnregisterGate.UnregisterAction();
ChatTwoRegisterGate.UnregisterFunc();
Registered.Clear();
}
}
+4 -1
View File
@@ -206,7 +206,10 @@ internal class MessageManager : IAsyncDisposable
Plugin.LogProxy.Error(ex, "Error in FilterAllTabs");
}
Plugin.LogProxy.Debug($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this
// without a Debug filter. Belt-and-suspenders for future plugin-load
// regressions; remains in place after Sub-Task 3.4 Befund.
Plugin.LogProxy.Information($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
});
}
File diff suppressed because it is too large Load Diff
+281 -12
View File
@@ -14,6 +14,7 @@ using HellionChat.Ipc;
using HellionChat.Resources;
using HellionChat.Ui;
using HellionChat.Util;
using Microsoft.Data.Sqlite;
namespace HellionChat;
@@ -122,11 +123,32 @@ 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;
// Set in the first DisposeAsync statement so async callbacks scheduled
// via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail
// before they touch state that has already been torn down. Volatile
// because the tick reads it from a different thread than the writer.
private volatile bool _isDisposing;
internal int DeferredSaveFrames = -1;
// Cancels the v1.4.8 FTS5 bulk-insert worker on plugin teardown. The
// worker runs off the framework thread on its own SqliteConnection, so a
// Dispose mid-rebuild must signal cancellation before MessageManager
// tears down (the worker logs "rebuild failed" via Log on error paths).
private CancellationTokenSource? _ftsRebuildCts;
// Serialises retention sweeps so a manual trigger and the 24h auto-sweep
// can't run in parallel. Volatile because the ImGui thread reads it outside
// the lock to gate the manual button.
@@ -176,8 +198,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (Config.Version < 16)
{
throw new InvalidOperationException(
$"HellionChat v1.4.7 requires config schema v16, got v{Config.Version}. "
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.7."
$"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;
@@ -221,6 +243,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
Directory.CreateDirectory(customThemesDir);
SeedExampleThemeIfEmpty(customThemesDir);
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
// Warm up the custom-theme cache before the first Switch.
// LoadCustomBySlug is a reverse-lookup over _customCache; on a
// cold cache a Config.Theme that points at a custom slug would
// fall through to the built-in default. AllCustom is a lazy
// enumerable, so iterate it explicitly to materialise the cache.
foreach (var _ in ThemeRegistry.AllCustom()) { }
ThemeRegistry.Switch(Config.Theme);
cancellationToken.ThrowIfCancellationRequested();
@@ -270,6 +298,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
cancellationToken.ThrowIfCancellationRequested();
// Populate the command dictionary + UiBuilder hooks BEFORE
// Commands.Initialise() walks the dictionary and registers each
// entry with Dalamud's CommandManager (Commands.cs:15-28). Adding
// wrappers after Initialise() would leak them — they'd live in
// the dictionary but never reach Dalamud.
SetupCommands();
Commands.Initialise();
// Daily retention sweep — fire-and-forget, skips when disabled
@@ -282,6 +316,113 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (Interface.Reason is not PluginLoadReason.Boot)
MessageManager.FilterAllTabsAsync();
// Kick the FTS5 rebuild worker if Migrate4 just added the schema or
// a previous run was cut short (InitFtsReadyCache leaves _ftsReady
// false in that case). Runs off the framework thread on its own
// SqliteConnection so the live UpsertMessage path keeps flowing
// through the chunked-commit windows.
_ftsRebuildCts = new CancellationTokenSource();
if (!MessageManager.Store.IsFtsIndexBuilt)
{
var token = _ftsRebuildCts.Token;
_ = Task.Run(
async () =>
{
// FQN: Plugin.Notification (Z.74) shadows the type name.
Dalamud.Interface.ImGuiNotification.IActiveNotification? notif = null;
try
{
notif = Notification.AddNotification(
new Dalamud.Interface.ImGuiNotification.Notification
{
Title = "Hellion Chat",
Content = "Indexing chat history for full-text search...",
Type = Dalamud
.Interface
.ImGuiNotification
.NotificationType
.Info,
Minimized = false,
InitialDuration = TimeSpan.FromMinutes(10),
}
);
// Progress<T> raises this callback on the captured
// sync-context (Task.Run worker pool). IActiveNotification
// is ImGui-backed and mutates the UI, so marshal the
// mutation onto the framework thread via RunOnTick.
var progress = new Progress<long>(done =>
{
Framework.RunOnTick(() =>
{
if (notif is { } n)
n.Content = $"Indexing chat history: {done:N0} messages...";
});
});
// Worker-owned connection. Closed+disposed before we
// flip the readiness flag so the DbViewer never sees
// IsFtsIndexBuilt=true while the worker connection
// is still alive.
SqliteConnection? workerConn = null;
try
{
workerConn = MessageManager.Store.OpenSecondaryConnection();
var total = await Task.Run(
() =>
MessageManager.Store.RebuildFtsIndex(
workerConn,
progress,
token
),
token
)
.ConfigureAwait(false);
workerConn.Close();
workerConn.Dispose();
workerConn = null;
MessageManager.Store.MarkFtsIndexBuilt();
if (notif is { } final)
{
final.Content = $"Indexed {total:N0} messages.";
final.Type = Dalamud
.Interface
.ImGuiNotification
.NotificationType
.Success;
final.InitialDuration = TimeSpan.FromSeconds(5);
}
}
finally
{
workerConn?.Dispose();
}
}
catch (OperationCanceledException)
{
notif?.DismissNow();
}
catch (Exception ex)
{
Log.Error(ex, "FTS index rebuild failed");
if (notif is { } err)
{
err.Content =
"Full-text indexing failed -- search will use local filter only.";
err.Type = Dalamud
.Interface
.ImGuiNotification
.NotificationType
.Error;
}
}
},
_ftsRebuildCts.Token
);
}
Interface.UiBuilder.DisableCutsceneUiHide = true;
Interface.UiBuilder.DisableGposeUiHide = true;
@@ -298,7 +439,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
Framework.Update += FrameworkUpdate;
Interface.UiBuilder.Draw += Draw;
Interface.LanguageChanged += LanguageChanged;
Interface.UiBuilder.OpenMainUi += OpenMainUi;
}
catch
{
@@ -320,14 +460,32 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
return;
// Set before any cleanup so deferred Framework.RunOnTick callbacks
// (B3 retention sweep) see the flag and bail out before they touch
// MessageManager / Log / static fields that the rest of this method
// is about to tear down.
_isDisposing = true;
Exception? failure = null;
// Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync.
failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi);
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
// Signal the FTS rebuild worker to bail. Runs before MessageManager
// tears down so the worker's "rebuild failed" log path still finds
// a live Log static. Worker owns its own SqliteConnection and disposes
// it itself; we only flip the cancellation flag here.
failure = CaptureFailure(
failure,
() =>
{
_ftsRebuildCts?.Cancel();
_ftsRebuildCts?.Dispose();
}
);
// Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore.
failure = CaptureFailure(
failure,
@@ -360,6 +518,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
await Framework
.RunOnFrameworkThread(() =>
{
// TearDown slash-commands + UiBuilder hooks before windows
// tear down. Slash-commands holding handlers that reach
// the windows would otherwise see a half-torn Plugin.
failure = CaptureFailure(failure, TearDownCommands);
failure = CaptureFailure(
failure,
() => GameFunctions.GameFunctions.SetChatInteractable(true)
@@ -538,11 +701,95 @@ public sealed class Plugin : IAsyncDalamudPlugin
}
}
private void OpenMainUi()
// Central slash-command + UiBuilder.OpenConfigUi/OpenMainUi subscribe so
// the four lazy windows (Settings, DbViewer, SeStringDebugger, Debugger)
// have working entry points before they're constructed.
private void SetupCommands()
{
SettingsWindow.IsOpen = !SettingsWindow.IsOpen;
// ChatLogWindow.cs:128 already registers /hellion (ToggleChat). The
// description-arg here keeps the Dalamud help list populated.
_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.
_hellionSeStringCmd = Commands.Register("/hellionSeString", showInHelp: false);
_hellionSeStringCmd.Execute += OnHellionSeStringCommand;
#endif
// Plugin-Manager "Settings" button. Was in Settings.cs:67 pre-v1.4.9.
Interface.UiBuilder.OpenConfigUi += OnOpenConfigUi;
// Plugin-Manager "Open" button. Was in Plugin.cs LoadAsync pre-v1.4.9
// (separate OpenMainUi handler that flipped SettingsWindow.IsOpen).
Interface.UiBuilder.OpenMainUi += OnOpenMainUi;
}
private void TearDownCommands()
{
Interface.UiBuilder.OpenMainUi -= OnOpenMainUi;
Interface.UiBuilder.OpenConfigUi -= OnOpenConfigUi;
// 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
if (_hellionSeStringCmd is not null)
{
_hellionSeStringCmd.Execute -= OnHellionSeStringCommand;
_hellionSeStringCmd = null;
}
#endif
}
private void OnHellionSettingsCommand(string command, string arguments)
{
// /hellion with args is intentionally a no-op (matches pre-v1.4.9
// Settings.cs:76-80 behaviour).
if (string.IsNullOrWhiteSpace(arguments))
SettingsWindow.Toggle();
}
private void OnOpenConfigUi() => SettingsWindow.Toggle();
private void OnOpenMainUi() => SettingsWindow.Toggle();
private void OnHellionViewCommand(string _, string __) => DbViewer.Toggle();
private void OnHellionDebuggerCommand(string _, string __) => DebuggerWindow.Toggle();
#if DEBUG
private void OnHellionSeStringCommand(string _, string __) => SeStringDebugger.Toggle();
#endif
private void RunRetentionSweepIfDue()
{
if (!Config.RetentionEnabled)
@@ -578,15 +825,31 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (deleted > 0)
{
Log.Information($"Retention sweep deleted {deleted} expired messages.");
// Run clear+refilter on the framework thread — FilterAllTabsAsync
// is fire-and-forget and would race the next sweep cycle.
Framework
.Run(() =>
// Schedule on the next framework tick to avoid the ~194ms
// hitch from blocking with .Wait() while the framework
// finishes the current frame. Tabs-list mutation must
// stay on the framework thread because Plugin.Config.Tabs
// (Configuration.cs:222) is not lock-protected and
// AutoTellTabsService can mutate it from background paths.
// Pattern reference: SimpleTweaks
// Tweaks/Chat/CaseInsensitiveCommands.cs:45.
Framework.RunOnTick(() =>
{
// The retention thread is IsBackground=true so plugin
// unload can fire while a scheduled tick is still
// pending; bail before touching anything torn down.
if (_isDisposing)
return;
try
{
MessageManager.ClearAllTabs();
MessageManager.FilterAllTabs();
})
.Wait();
}
catch (Exception ex)
{
Log.Error(ex, "Retention sweep clear+refilter failed");
}
});
}
else
{
@@ -610,6 +873,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
private void Draw()
{
// v1.4.8 B2: pick up external edits of the active custom theme JSON
// without forcing the user to re-click the picker. The disk-stat is
// 1Hz-throttled inside RefreshActiveIfStale, so this is essentially
// free on built-in themes and ~1 stat/second on custom themes.
ThemeRegistry.RefreshActiveIfStale();
// Theme engine is always active; Classic is a theme, not a disabled state.
using IDisposable _style = HellionStyle.PushGlobal(
ThemeRegistry.Active,
+9
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));
@@ -402,4 +406,9 @@ internal class HellionStrings
// Hellion Chat — v1.3.0 Honorific title slot tooltip
internal static string ChatHeader_HonorificTitle_Tooltip => Get(nameof(ChatHeader_HonorificTitle_Tooltip));
// Hellion Chat — v1.4.8 DbViewer full-text search toggle
internal static string DbViewer_FullTextToggle => Get(nameof(DbViewer_FullTextToggle));
internal static string DbViewer_FullTextToggle_Hint_Indexing => Get(nameof(DbViewer_FullTextToggle_Hint_Indexing));
internal static string DbViewer_FullTextToggle_Hint_PhraseMode => Get(nameof(DbViewer_FullTextToggle_Hint_PhraseMode));
}
@@ -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>
@@ -917,4 +925,13 @@
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
<value>Custom-Titel von Honorific</value>
</data>
<data name="DbViewer_FullTextToggle" xml:space="preserve">
<value>Volltext-Suche</value>
</data>
<data name="DbViewer_FullTextToggle_Hint_Indexing" xml:space="preserve">
<value>Der Volltext-Index wird noch gebaut. Die lokale Suche bleibt verfügbar.</value>
</data>
<data name="DbViewer_FullTextToggle_Hint_PhraseMode" xml:space="preserve">
<value>Sucht nach der exakten Wortfolge. Mehrere Wörter werden nur gefunden, wenn sie zusammen und in dieser Reihenfolge stehen. Wer rohe FTS5-MATCH-Syntax nutzen will, setzt eigene Anführungszeichen um den Suchbegriff.</value>
</data>
</root>
+17
View File
@@ -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>
@@ -917,4 +925,13 @@
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
<value>Custom title from Honorific</value>
</data>
<data name="DbViewer_FullTextToggle" xml:space="preserve">
<value>Full-text search</value>
</data>
<data name="DbViewer_FullTextToggle_Hint_Indexing" xml:space="preserve">
<value>The full-text index is still being built. The local filter remains available.</value>
</data>
<data name="DbViewer_FullTextToggle_Hint_PhraseMode" xml:space="preserve">
<value>Searches for the exact phrase. Multi-word queries match only when the words appear together in order. To use raw FTS5 MATCH syntax, wrap your term in double quotes yourself.</value>
</data>
</root>
+99 -11
View File
@@ -6,6 +6,13 @@ public sealed class ThemeRegistry
{
public const string DefaultSlug = HellionArctic.Slug;
// 1Hz throttle for the v1.4.8 B2 auto-refresh-on-active path. The
// Plugin.Draw hook calls RefreshActiveIfStale every frame, but the
// actual File.GetLastWriteTimeUtc disk-stat only runs once per second
// -- 60fps would otherwise mean 3600 stats/min on the same path (more
// on Wine). Same idiom as the StatusBar 1Hz cache.
private const long ActiveStampPollIntervalMs = 1000;
private readonly Dictionary<string, Theme> _builtIns;
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(
StringComparer.OrdinalIgnoreCase
@@ -13,6 +20,15 @@ public sealed class ThemeRegistry
private readonly string? _customThemesDir;
private Theme _active;
// v1.4.8 B2: source path of the currently active custom theme. Captured
// at Switch() time so RefreshActiveIfStale does not have to reconstruct
// 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
// theme is built-in or no custom-themes directory is configured.
private string? _activeCustomPath;
private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs;
private DateTime _lastActiveStamp = DateTime.MinValue;
public ThemeRegistry(string? customThemesDir = null)
{
// Insertion order drives the Theme-Picker grid layout (3 columns).
@@ -48,7 +64,9 @@ public sealed class ThemeRegistry
if (_builtIns.TryGetValue(slug, out var b))
return b;
var custom = LoadCustomBySlug(slug);
// Discard the source path here; Switch is the only call-site that
// needs to remember it for the auto-refresh hook.
var custom = LoadCustomBySlug(slug, out _);
if (custom != null)
return custom;
@@ -59,12 +77,70 @@ public sealed class ThemeRegistry
public IEnumerable<Theme> AllCustom() => RefreshCustomCache();
// Built-in-first to match Get(slug)'s lookup order. A user theme JSON
// that declares the same slug as a built-in is ignored deliberately --
// having Switch prefer custom and Get prefer built-in would produce
// a state where _active and Get(_active.Slug) disagree.
public void Switch(string slug)
{
var theme = Get(slug);
// Defensive — ensures any future theme source always gets a populated cache.
theme.RecomputeAbgrCache();
_active = theme;
if (_builtIns.TryGetValue(slug, out var builtin))
{
_active = builtin;
_active.RecomputeAbgrCache();
_activeCustomPath = null;
return;
}
var customTheme = LoadCustomBySlug(slug, out var customPath);
if (customTheme is not null)
{
_active = customTheme;
// Defensive — ensures any future theme source always gets a populated cache.
_active.RecomputeAbgrCache();
_activeCustomPath = customPath;
// Force a first-tick reload-check after the switch so the stamp
// baseline is established on the next RefreshActiveIfStale call.
_lastActiveStamp = DateTime.MinValue;
return;
}
// Fallback: neither built-in nor custom matched. Drop to default
// and clear the active custom path so RefreshActiveIfStale stays idle.
_active = _builtIns[DefaultSlug];
_active.RecomputeAbgrCache();
_activeCustomPath = null;
}
// 1Hz-throttled disk-stat on the currently active custom theme file.
// When the file's LastWriteTime moves forward (editor save), reload the
// theme via Get() so the user sees the edit immediately without
// re-selecting in the picker. Built-in themes short-circuit; custom
// themes without an _activeCustomPath (e.g. Switch fell to default)
// short-circuit too.
public void RefreshActiveIfStale()
{
var now = Environment.TickCount64;
if (now - _lastActiveStampCheckMs < ActiveStampPollIntervalMs)
return;
_lastActiveStampCheckMs = now;
if (_active.IsBuiltIn)
return;
var path = _activeCustomPath;
if (path is null || !File.Exists(path))
return;
var stamp = File.GetLastWriteTimeUtc(path);
if (!ThemeStampDiff.IsStale(_lastActiveStamp, stamp))
return;
_lastActiveStamp = stamp;
// Get() re-runs RefreshCustomCache which picks up the new content
// (the cache keys by path + LastWriteTime, so a mtime bump invalidates).
// RecomputeAbgrCache happens inside RefreshCustomCache on cache miss.
var reloaded = Get(_active.Slug);
_active = reloaded;
}
// 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION.
@@ -77,18 +153,30 @@ public sealed class ThemeRegistry
return code == 0x80070020u || code == 0x80070021u;
}
// Custom themes are loaded lazily, cached by LastWriteTime.
// A changed JSON is reloaded on the next lookup.
private Theme? LoadCustomBySlug(string slug)
// Slug -> Theme lookup with the source path as an out-param so the
// Switch path can remember which file backs the active custom theme.
// Pure reverse-lookup over the existing _customCache: that cache is
// already Path -> (Theme, Stamp), so iterating it costs nothing,
// avoids a re-parse of every JSON, and keeps the parse logic (and
// the recoverable-file-lock recovery) confined to RefreshCustomCache.
// The cache must be warm before this runs; Plugin.LoadAsync triggers
// a one-time warm-up via AllCustom() before the first Switch call.
private Theme? LoadCustomBySlug(string slug, out string? sourcePath)
{
sourcePath = null;
if (_customThemesDir is null)
return null;
if (!Directory.Exists(_customThemesDir))
return null;
foreach (var theme in RefreshCustomCache())
if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
return theme;
foreach (var kvp in _customCache)
{
if (string.Equals(kvp.Value.Theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
{
sourcePath = kvp.Key;
return kvp.Value.Theme;
}
}
return null;
}
+20
View File
@@ -0,0 +1,20 @@
namespace HellionChat.Themes;
// Pure stale-check for the v1.4.8 B2 theme-auto-refresh-on-active path.
// Lives in a free helper class so the Build-Suite can exercise the diff
// rules without instantiating ThemeRegistry (which touches the Dalamud
// log proxy and the filesystem). The rules:
// - DateTime.MinValue on the current stat means we could not read the
// file -- hold the last known good (return false).
// - Equal stamps mean no change since we last saw it.
// - Any other difference, including the first observation where lastSeen
// is MinValue, counts as stale and triggers a reload.
internal static class ThemeStampDiff
{
public static bool IsStale(System.DateTime lastSeen, System.DateTime current)
{
if (current == System.DateTime.MinValue)
return false;
return current != lastSeen;
}
}
+101 -17
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;
@@ -419,8 +422,9 @@ public sealed class ChatLogWindow : Window
// The hint banner renders before this block so ImGui already accounts for it.
height -= ImGui.GetFrameHeightWithSpacing();
// Status bar at the window bottom reserves 22px + 2px spacing.
height -= StatusBar.Height + 2;
// StatusBar.Height now bakes in its own DPI-aware 2px spacer, so the
// window reservation is just Height -- no extra +2 (v1.4.8 B1).
height -= StatusBar.Height;
return height;
}
@@ -635,6 +639,15 @@ public sealed class ChatLogWindow : Window
IsOpen = true;
}
// v1.4.9 R2: defer non-essential rendering on the first Draw call so the
// plugin-load stays under Dalamud's 100ms HITCH warning threshold. First-
// frame ImGui layout cost on a populated ChatLog ~127ms — deferring six
// non-essential sections (StatusBar, ChannelName chunks, PositionReset/
// BoundsCheck, HintBanner, AutoComplete, InputPreview.CalculatePreview)
// shaves ~33ms down to ~94ms. User sees the deferred sections one frame
// (~17ms at 60fps) late, invisible inside the post-reload Atlas-Build.
private bool _firstFrameDone;
public override void Draw()
{
DrewThisFrame = true;
@@ -642,7 +655,11 @@ public sealed class ChatLogWindow : Window
{
DrawChatLog();
AddPopOutsToDraw();
DrawAutoComplete();
// v1.4.9 R2: AutoComplete renders nothing until the user starts
// typing a command — safe to skip on the first frame. ~6ms.
if (_firstFrameDone)
DrawAutoComplete();
}
catch (Exception ex)
{
@@ -664,6 +681,13 @@ public sealed class ChatLogWindow : Window
// input focus, which breaks every other ImGui window.
Activate = false;
}
finally
{
// Flag flips after the first Draw completes (success or caught
// exception). Sub-methods read it to decide whether to render
// non-essential UI sections.
_firstFrameDone = true;
}
}
private static bool IsChatMode =>
@@ -679,18 +703,25 @@ public sealed class ChatLogWindow : Window
LastWindowSize = currentSize;
LastWindowPos = ImGui.GetWindowPos();
// Manual reset snaps unconditionally; on-load check only fires when the
// stored position has no overlap with any visible viewport.
if (RequestPositionReset)
// v1.4.9 R2: skip the bounds-check chain on the first frame. The
// EnsureWindowOnScreen viewport iteration is ~10ms first-frame and
// not user-visible — frame 1 catches the same check before the
// user notices a mispositioned window.
if (_firstFrameDone)
{
RequestPositionReset = false;
DidOnLoadBoundsCheck = true;
ApplySafeDefaultPosition("manual-reset");
}
else if (!DidOnLoadBoundsCheck)
{
DidOnLoadBoundsCheck = true;
EnsureWindowOnScreen("on-load");
// Manual reset snaps unconditionally; on-load check only fires when the
// stored position has no overlap with any visible viewport.
if (RequestPositionReset)
{
RequestPositionReset = false;
DidOnLoadBoundsCheck = true;
ApplySafeDefaultPosition("manual-reset");
}
else if (!DidOnLoadBoundsCheck)
{
DidOnLoadBoundsCheck = true;
EnsureWindowOnScreen("on-load");
}
}
if (resized)
@@ -699,12 +730,17 @@ public sealed class ChatLogWindow : Window
LastViewport = ImGui.GetWindowViewport().Handle;
WasDocked = ImGui.IsWindowDocked();
if (IsChatMode && Plugin.InputPreview.IsDrawable)
// v1.4.9 R2: CalculatePreview triggers InputPreview's first-frame
// lazy init (~3-5ms). User-typing-driven, safe to defer one frame.
if (_firstFrameDone && IsChatMode && Plugin.InputPreview.IsDrawable)
Plugin.InputPreview.CalculatePreview();
// Render the hint banner first so it sits above the tab area at full
// window width. ImGui accounts for its height automatically.
DrawV061HintBannerIfNeeded();
// v1.4.9 R2: skip on first frame (~3-5ms layout cost). The banner
// is a v0.6.1 migration notice that returns the same result frame 1.
if (_firstFrameDone)
DrawV061HintBannerIfNeeded();
if (Plugin.Config.SidebarTabView)
DrawTabSidebar();
@@ -759,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;
@@ -937,7 +1007,11 @@ public sealed class ChatLogWindow : Window
// v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog,
// damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen.
Plugin.StatusBar.Draw(Plugin);
// v1.4.9 R2: skip on the first frame; ~12ms of first-frame layout
// cost. User sees the StatusBar 1 frame (~17ms at 60fps) later
// which is hidden inside the post-reload Atlas-Build window.
if (_firstFrameDone)
Plugin.StatusBar.Draw(Plugin);
}
internal Dictionary<string, InputChannel> GetValidChannels()
@@ -988,6 +1062,16 @@ public sealed class ChatLogWindow : Window
private void DrawChannelName(Tab activeTab)
{
// v1.4.9 R2: plain-text fallback on the first frame. ReadChannelName
// builds SeString chunks and DrawChunks runs SeString-Renderer layout
// — together ~18ms first-frame. Frame 1 renders the real chunks; the
// user sees the tab name for ~17ms during the post-reload window.
if (!_firstFrameDone)
{
ImGui.TextUnformatted(activeTab.Name);
return;
}
var currentChannel = ReadChannelName(activeTab);
if (!currentChannel.SequenceEqual(PreviousChannel))
PreviousChannel = currentChannel;
+73 -18
View File
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Numerics;
using System.Text;
using System.Threading;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
@@ -33,11 +34,21 @@ public class DbViewer : Window
private int CurrentPage = 1;
private string SimpleSearchTerm = "";
// v1.4.8 H2: opt-in full-text search across the whole DB via FTS5.
// Transient UI state (per-session), not persisted -- users opt in fresh
// every time so they always see the page-filter as the default mode.
private bool UseFullTextSearch;
private bool OnlyCurrentCharacter = true;
private readonly Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels;
private bool IsProcessing;
private long ProcessingStart = Environment.TickCount64;
// Bumped per trigger so a late worker drops itself instead of overwriting
// a newer result.
private long _ftsFilterSeq;
private (DateTime Min, DateTime Max, int Page, bool Local, int ChannelCount) LastProcessed;
private string MinDateString = "";
@@ -82,29 +93,13 @@ public class DbViewer : Window
RespectCloseHotkey = false;
DisableWindowSounds = true;
Plugin
.Commands.Register(
"/hellionView",
"Get access to your message history, with simple filter options.",
true
)
.Execute += Toggle;
}
public void Dispose()
{
Plugin
.Commands.Register(
"/hellionView",
"Get access to your message history, with simple filter options.",
true
)
.Execute -= Toggle;
// Slash-command tear-down moved to Plugin.TearDownCommands.
}
private void Toggle(string _, string __) => Toggle();
public override void Draw()
{
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
@@ -233,6 +228,24 @@ public class DbViewer : Window
tooltipRight: Language.Page_ArrowRight_Tooltip
);
// Full-text search toggle (v1.4.8 H2). IsFtsIndexBuilt is a cached
// volatile bool in MessageStore -- single field read per frame, no
// SELECT count(*). ImRaii.Disabled blocks any click while the index
// is still being built, so no defensive force-off branch needed
// inside the if-body. UseFullTextSearch is transient UI state, so we
// do not call SaveConfig here.
var ftsReady = Plugin.MessageManager.Store.IsFtsIndexBuilt;
using (ImRaii.Disabled(!ftsReady))
{
if (ImGui.Checkbox(HellionStrings.DbViewer_FullTextToggle, ref UseFullTextSearch))
TriggerFilterRefresh();
}
ImGuiUtil.HelpMarker(
ftsReady
? HellionStrings.DbViewer_FullTextToggle_Hint_PhraseMode
: HellionStrings.DbViewer_FullTextToggle_Hint_Indexing
);
ImGui.SameLine(ImGui.GetContentRegionMax().X - width);
ImGui.SetNextItemWidth(width);
if (
@@ -243,7 +256,7 @@ public class DbViewer : Window
30
)
)
Filtered = Filter(Messages);
TriggerFilterRefresh();
// Third row
@@ -447,11 +460,53 @@ public class DbViewer : Window
}
}
// FTS path hits SQLite per keystroke -- dispatch to a worker, drop stale
// results via _ftsFilterSeq. Page-filter path is in-memory LINQ, stays
// inline.
private void TriggerFilterRefresh()
{
if (!UseFullTextSearch || !Plugin.MessageManager.Store.IsFtsIndexBuilt)
{
Filtered = Filter(Messages);
return;
}
var snapshot = Messages;
var mySeq = Interlocked.Increment(ref _ftsFilterSeq);
Task.Run(() =>
{
try
{
var result = Filter(snapshot);
if (Interlocked.Read(ref _ftsFilterSeq) == mySeq)
Filtered = result;
}
catch (Exception ex)
{
Plugin.LogProxy.Error(ex, "FTS filter worker failed");
}
});
}
private ConcurrentStack<Message> Filter(Message[] messages)
{
if (SimpleSearchTerm == "")
return new ConcurrentStack<Message>(messages.Reverse().OrderByDescending(m => m.Date));
// Full-text mode bypasses the page-bounded messages array and queries
// the FTS5 index across the whole DB. IsFtsIndexBuilt re-check guards
// against the (rare) case of the toggle being on while the index is
// mid-rebuild -- ImRaii.Disabled prevents the user from flipping it,
// but a Dispose-and-reopen during indexing could leave UseFullTextSearch
// true while ftsReady flipped back to false; the local fallback below
// still serves the page.
if (UseFullTextSearch && Plugin.MessageManager.Store.IsFtsIndexBuilt)
{
var guidHits = Plugin.MessageManager.Store.FullTextSearch(SimpleSearchTerm);
var resolved = Plugin.MessageManager.Store.LoadByGuids(guidHits);
return new ConcurrentStack<Message>(resolved.OrderByDescending(m => m.Date));
}
return new ConcurrentStack<Message>(
messages
.Reverse()
+1 -5
View File
@@ -28,17 +28,13 @@ public class DebuggerWindow : Window, IDisposable
RespectCloseHotkey = false;
DisableWindowSounds = true;
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute += Toggle;
}
public void Dispose()
{
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute -= Toggle;
// Slash-command tear-down moved to Plugin.TearDownCommands.
}
private void Toggle(string _, string __) => Toggle();
public override unsafe void Draw()
{
var agent = (nint)AgentItemDetail.Instance();
+1 -9
View File
@@ -29,21 +29,13 @@ public class SeStringDebugger : Window
RespectCloseHotkey = false;
DisableWindowSounds = true;
#if DEBUG
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
#endif
}
public void Dispose()
{
#if DEBUG
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
#endif
// Slash-command tear-down moved to Plugin.TearDownCommands.
}
private void Toggle(string _, string __) => Toggle();
public override void Draw()
{
if (Plugin.MessageManager.LastMessage.Sender == null)
+1 -13
View File
@@ -60,23 +60,11 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
DisableWindowSounds = true;
Initialise();
Plugin
.Commands.Register("/hellion", "Perform various actions with Hellion Chat.")
.Execute += Command;
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
}
public void Dispose()
{
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
Plugin.Commands.Register("/hellion").Execute -= Command;
}
private void Command(string command, string args)
{
if (string.IsNullOrWhiteSpace(args))
Toggle();
// Slash-command + OpenConfigUi tear-down moved to Plugin.TearDownCommands.
}
private void Initialise()
+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);
}
}
+13 -4
View File
@@ -2,6 +2,7 @@ using System.Globalization;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Code;
using HellionChat.Resources;
@@ -9,12 +10,20 @@ using HellionChat.Util;
namespace HellionChat.Ui;
// Bottom status bar, 22px tall. Slots left to right: channel indicator,
// privacy badge, counts, tells (hidden at 0), version (right-aligned).
// Updates at 1Hz; format strings are cached between updates.
// Bottom status bar. Slots left to right: channel indicator, privacy badge,
// counts, tells (hidden at 0), version (right-aligned). Updates at 1Hz;
// format strings are cached between updates.
internal sealed class StatusBar
{
public const float Height = 22f;
// DPI-aware bar height. The previous fixed 22px constant clipped on
// Windows display-scaling >100% because ImGui renders the font bigger
// than the reservation. GetTextLineHeightWithSpacing scales with the
// current ImGui font; the 2px spacer is GlobalScale-rounded to stay
// on integer pixel boundaries (same idiom as v1.4.6 F7.2 underline-pill
// in ChatLogWindow.cs:1639-1653).
public static float Height =>
ImGui.GetTextLineHeightWithSpacing() + MathF.Round(2f * ImGuiHelpers.GlobalScale);
private const long UpdateIntervalMs = 1000;
// Initially outdated so the first frame always computes fresh.
+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);
}
}
+6 -1
View File
@@ -62,7 +62,12 @@ internal static class AutoTranslate
{
var sw = Stopwatch.StartNew();
AllEntries();
Plugin.LogProxy.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this
// without a Debug filter. Belt-and-suspenders for future plugin-load
// regressions; remains in place after Sub-Task 3.4 Befund.
Plugin.LogProxy.Information(
$"Warming up auto-translate took {sw.ElapsedMilliseconds}ms"
);
})
{
IsBackground = true,
+19 -19
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.7-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.7** — 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,23 +286,23 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
## Project Status
**Version 1.4.7**Backlog cleanup and the first user-visible feature bundle since v1.4.5. TempTell tabs can now be
pinned via right-click; pinned tabs survive relog, keep their conversation history (loaded on demand from the message
store), and stay bound to the same `/tell` partner. A hard cap of 5 pinned tabs lives in a pool separate from the 15-tab
auto-tell pool, so the total ceiling is 20 tabs. The sidebar groups pinned tabs into their own section with its own
divider header. Honorific glow outlines now render when the title carries a Glow colour — opt-in via **Settings →
Integrations → Render glow outlines (Honorific)**, default off, so v1.4.6 visuals stay untouched for users who don't
care and the per-frame DrawList overhead is skipped on low-end hardware. Honorific gradient (Color3 / GradientColourSet
/ Wave / Pulse) is parsed and stashed for a later cycle, but currently renders as the primary colour. Sidebar width is
configurable in **Theme & Layout** between 44 and 160 px; default stays icon-only so existing users see no layout
change. `Configuration.UpdateFrom` now preserves the runtime `CurrentChannel` across the persistent-tab merge, and
`TabSwitched` deep-clones the seeded channel — together they fix a Settings-Save regression where the chat input could
pop back to `/tell <pinned-partner>` after touching settings while on a Party or Linkshell tab. Internal items:
`IPluginLogProxy` indirection over Dalamud's `IPluginLog` routes all ~91 `Plugin.Log` call sites through a testable
proxy, closing the test-isolation gap F12.1 left in v1.4.6 (`MessageStore.Migrate0` now runs in xUnit without loading
`Dalamud.dll`). `Util/ImGuiUtil.cs`'s `DrawArrows` IconButton id gets explicit parentheses on the increment. Migration
v16 → v17 is additive (new `Tab.IsPinned` flag, default false). Eighth sub-patch of the v1.4.x polish sweep series (as
of 2026-05-13).
**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:
+90
View File
@@ -10,6 +10,96 @@ 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
~76 ms median — comfortably under Dalamud's 100 ms HITCH warning threshold. The remaining ~13 ms gap to ChatTwo
upstream (~63 ms median) is the cost of HellionChat-only features (sidebar tab view, custom status bar,
Honorific integration).
- First-frame defer: six non-essential rendering sections inside `ChatLogWindow` skip their first Draw and run
one frame later. Covered sections are the bottom status bar, channel-name SeString chunks, window bounds
check, v0.6.1 hint banner, autocomplete and input-preview calculation. At 60 fps the user sees those sections
~17 ms after plugin reload — invisible inside the ~2.5 s font-atlas build window every reload runs through
anyway. Frame 1 stays well under 100 ms too (~40 ms), so no secondary HITCH warning appears.
- Slash-command centralisation: `/hellion`, `/hellionView`, `/hellionSeString` and `/hellionDebugger` are now
registered during `LoadAsync` instead of inside the corresponding window constructors. The commands work
before their target window is opened the first time, and Dalamud's plugin-manager configuration / open
buttons (`UiBuilder.OpenConfigUi` / `OpenMainUi`) hang on the same path.
- Plugin-load profiling logs stay on: `MessageStore.Connect`, `MessageStore.Migrate`, `FilterAllTabs` and the
auto-translate warm-up timing log are now Information level rather than Debug. They serve as a tripwire so a
future regression past 100 ms shows up directly in `/xllog` without re-enabling Debug.
- 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. Third-party
integrations that historically only subscribe to ChatTwo's IPC — for example Artisan's and AllaganTools'
context-menu hooks — 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).
- Internal: hypothesis-triage during the R2 cycle falsified three of the four candidate root causes
(font-atlas sync, theme-apply ABGR-cache init, multiple-window render). Actual cause is `DrawList` setup
cost distributed across ~10 ImGui sections inside ChatLogWindow (5-20 ms each). The six selective defers
above are the pragmatic fix — a clean structural rewrite would belong in the v1.5.x DI-container cycle.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
## Hellion Chat 1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)
Ninth sub-patch of the v1.4.x polish-sweep series. Hook-layer cluster (FTS5 full-text search, ad-block foundation
investigation) plus three polish quick-wins.
- DbViewer full-text search: optional FTS5 index across the full chat history. Built asynchronously on first run after
the update with a progress toast (UI stays responsive, the toggle is disabled until the build completes). The local
page-filter remains the default mode. Multi-word queries match as exact phrases; power users can opt into raw FTS5
`MATCH` syntax by wrapping their own double-quotes around the term.
- Custom theme files now auto-reload when edited while the theme is active. Save the JSON in your editor and the live
render picks up the change within a second — no need to re-click the theme in the picker. Disk-stat is throttled to
1 Hz so per-frame cost stays free.
- Retention sweep no longer blocks the framework thread. `Framework.Run(...).Wait()` is replaced by
`Framework.RunOnTick(...)`, which removes the ~194 ms hitch the sweep used to add per run.
- Status bar height is derived from `GetTextLineHeightWithSpacing()` plus a DPI-aware spacer so the bar renders
correctly at Windows display scaling above 100 %. Linux/Wayland default of 100 % is unaffected.
- Receive-suppressed-tells routing was investigated this cycle and **postponed to v1.5.x**. When other plugins suppress
tells via `CheckMessageHandled`, FFXIV's chat pipeline skips the `RaptureLogModule.AddMsgSourceEntry` path, which means
HellionChat's `ContentIdResolverHook` does not fire and tell-partner identification breaks for AutoTellTab routing.
The proper fix sits next to the planned ad-block hook layer (`RaptureLogModule.ShowMiniTalkPlayer` and friends) where
the same patch surface comes up anyway.
- Internal: storage form of `messages.Id` clarified (declared BLOB but Microsoft.Data.Sqlite stores Guid parameters as
TEXT). FTS bulk insert and `LoadByGuids` join now match the TEXT storage form on both sides. Migration v17 stays
(no schema bump).
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
## Hellion Chat 1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)
Eighth sub-patch of the v1.4.x polish-sweep series. First user-visible feature bundle since v1.4.5 — pinned tell tabs
+60 -4
View File
@@ -10,11 +10,67 @@ the plugin's privacy-first scope during brainstorming.
---
## Next Cycle (v1.4.8)
## Next Cycle (v1.5.0)
**Hook-Layer Cycle.** Receive-suppressed-tells toggle (cross-reference XIVIM #73 bubble-layer sub-task), Database Viewer
full-text search via SQLite FTS5, plus preparation for the later Ad-Block cycle. Hook-layer investigation is shared
across these items so they cluster naturally in one sub-patch.
**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.
---
## v1.4.9 — Plugin-Load Render Polish (released 2026-05-15)
Tenth sub-patch of the v1.4.x Polish Sweep series. First-frame HITCH drops from ~127 ms median to ~76 ms median (4-reload
sample), comfortably under Dalamud's 100 ms warning threshold. Mechanism: a single `_firstFrameDone` flag inside
`ChatLogWindow` defers six non-essential rendering sections (bottom status bar, channel-name SeString chunks, window
bounds check, v0.6.1 hint banner, autocomplete, input-preview calculation) from frame 0 to frame 1. User sees those
sections ~17 ms (60 fps) later, invisible inside the ~2.5 s font-atlas build window after every reload. Slash-command
registration moved from individual window constructors to a central `SetupCommands` / `TearDownCommands` pair in
`Plugin.cs``/hellion`, `/hellionView`, `/hellionSeString` and `/hellionDebugger` work before their target windows are
opened the first time, and Dalamud's plugin-manager `OpenConfigUi` / `OpenMainUi` buttons hang on the same path.
Plugin-load profiling logs (auto-translate warmup, `MessageStore.Connect`, `MessageStore.Migrate`, `FilterAllTabs`) stay
on at Information level as a regression tripwire. The release also ships a ChatTwo IPC compatibility layer: HellionChat
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 only subscribe to ChatTwo's IPC (Artisan, AllaganTools) keep working without a code change on their
side. Conflict detection prevents ChatTwo from loading in parallel, so there is no slot-collision risk at runtime.
Migration v17 stays (no schema bump). Hypothesis-triage falsified
three of four candidate root causes (font-atlas sync fallback, theme-apply ABGR-cache init, multiple-window render via
lazy-init) — actual cost distributes evenly across ~10 ImGui sections inside ChatLogWindow, so structural rewrite is
deferred to v1.5.x DI-container cycle.
## v1.4.8 — Hook-Layer and Polish Quick-Wins (released 2026-05-14)
Ninth sub-patch of the v1.4.x Polish Sweep series. Database Viewer gains an optional FTS5 full-text search across the
full chat history, built asynchronously on first run after the update with a progress toast; the local page-filter
remains the default mode. Custom theme files auto-reload when edited while the theme is active (1 Hz disk-stat throttle,
so per-frame cost is free). Retention sweep no longer blocks the framework thread — `Framework.Run(...).Wait()` is
replaced by `Framework.RunOnTick(...)`, removing the ~194 ms hitch per sweep. Status-bar height is now derived from
`GetTextLineHeightWithSpacing()` plus a DPI-aware spacer so the bar renders correctly at Windows display scaling above
100 %. Receive-suppressed-tells routing was investigated and **postponed to v1.5.x**: when other plugins suppress tells
via `CheckMessageHandled`, FFXIV's chat-pipeline skips the `RaptureLogModule.AddMsgSourceEntry` path, which means the
`ContentIdResolverHook` does not fire and tell-partner identification breaks. The fix belongs next to the planned ad-block
hook layer where the same patch surface comes up anyway. Migration v17 stays (no schema bump). H3 leaves a foundation
note in the Vault (`Projekte/FFXIV/Hellion Chat/v1.5.x Ad-Block Foundation.md`) covering the NoSoliciting filter +
bubble-layer hook pattern as a ready-made template for the v1.5.x cycle.
---
+6 -6
View File
File diff suppressed because one or more lines are too long