Compare commits

...

9 Commits

Author SHA1 Message Date
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
18 changed files with 414 additions and 116 deletions
+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.
+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.8</Version>
<Version>1.4.9</Version>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Use lock file to pin exact versions -->
+38 -23
View File
@@ -35,6 +35,44 @@ tags:
- Replacement
- Privacy
changelog: |-
**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
@@ -151,27 +189,4 @@ changelog: |-
---
**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
---
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");
});
}
+15
View File
@@ -237,14 +237,26 @@ internal class MessageStore : IDisposable
private SqliteConnection Connect()
{
// v1.4.9 R3 profiling: trace cost of SQLite open + pragma-apply. Paired
// with the Migrate-Stopwatch below — Connect alone is the cheap half
// (Open + a handful of PRAGMAs); the expensive half typically lives in
// Migrate, especially on a large DB after a schema bump.
var connectSw = System.Diagnostics.Stopwatch.StartNew();
var conn = new SqliteConnection(BuildConnectionString(DbPath));
conn.Open();
ApplyPragmas(conn);
connectSw.Stop();
_logger.Information($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms");
return conn;
}
private void Migrate()
{
// v1.4.9 R3 profiling: trace cost of the schema-migration chain. On a
// large DB after a fresh schema bump this is the dominant SQLite cost
// at plugin-load, not Connect.
var migrateSw = System.Diagnostics.Stopwatch.StartNew();
using var cmd = Connection.CreateCommand();
cmd.CommandText = "PRAGMA user_version;";
var userVersion = Convert.ToInt32(cmd.ExecuteScalar());
@@ -276,6 +288,9 @@ internal class MessageStore : IDisposable
foreach (var migration in migrationsToDo)
migration();
migrateSw.Stop();
_logger.Information($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms");
}
private void Migrate0()
+84 -6
View File
@@ -189,8 +189,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (Config.Version < 16)
{
throw new InvalidOperationException(
$"HellionChat v1.4.8 requires config schema v16, got v{Config.Version}. "
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.8."
$"HellionChat v1.4.9 requires config schema v16, got v{Config.Version}. "
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.9."
);
}
Config.Version = 17;
@@ -289,6 +289,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
@@ -424,7 +430,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
Framework.Update += FrameworkUpdate;
Interface.UiBuilder.Draw += Draw;
Interface.LanguageChanged += LanguageChanged;
Interface.UiBuilder.OpenMainUi += OpenMainUi;
}
catch
{
@@ -455,7 +460,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
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);
@@ -505,6 +509,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)
@@ -683,11 +692,80 @@ 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.
Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute +=
OnHellionSettingsCommand;
Commands
.Register(
"/hellionView",
"Get access to your message history, with simple filter options.",
true
)
.Execute += OnHellionViewCommand;
Commands.Register("/hellionDebugger", showInHelp: false).Execute +=
OnHellionDebuggerCommand;
#if DEBUG
// SeStringDebugger.cs lives under #if DEBUG too; keep this out of release builds.
Commands.Register("/hellionSeString", showInHelp: false).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;
Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute -=
OnHellionSettingsCommand;
Commands
.Register(
"/hellionView",
"Get access to your message history, with simple filter options.",
true
)
.Execute -= OnHellionViewCommand;
Commands.Register("/hellionDebugger", showInHelp: false).Execute -=
OnHellionDebuggerCommand;
#if DEBUG
Commands.Register("/hellionSeString", showInHelp: false).Execute -=
OnHellionSeStringCommand;
#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)
+61 -15
View File
@@ -636,6 +636,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;
@@ -643,7 +652,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)
{
@@ -665,6 +678,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 =>
@@ -680,18 +700,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)
@@ -700,12 +727,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();
@@ -938,7 +970,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()
@@ -989,6 +1025,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;
+1 -17
View File
@@ -93,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);
+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 -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,
+20 -15
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.8-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
[![Latest release](https://img.shields.io/badge/release-v1.4.9-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.8** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
**Version 1.4.9** — 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,19 +286,24 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
## Project Status
**Version 1.4.8**Hook-Layer and Polish Quick-Wins. The Database Viewer now has an optional FTS5 full-text search
across the entire chat history. Toggle "Full-text search" next to the search bar; the index is built asynchronously on
first run after the update with a progress toast, and the toggle stays disabled until the build completes. Multi-word
terms match as exact phrases by default; power users can opt into raw FTS5 `MATCH` syntax by wrapping their own
double-quotes. Custom theme files 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 picker click. Retention sweep no longer blocks the framework
thread (`Framework.Run(...).Wait()` 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 is postponed to v1.5.x; the investigation in this
cycle showed that the FFXIV `ContentIdResolverHook` does not fire when other plugins suppress tells via
`CheckMessageHandled`, which means tell-partner identification breaks for AutoTellTab routing — the fix lives next to
the planned ad-block hook layer where the same `RaptureLogModule` patch surface comes up anyway. Migration v17 stays
(no schema bump). Ninth sub-patch of the v1.4.x polish sweep series (as of 2026-05-14).
**Version 1.4.9**Plugin-Load Render Polish. First-frame render cost is now well under Dalamud's 100 ms HITCH
warning threshold (~76 ms median, down from ~127 ms). The gain comes from deferring six non-essential rendering
sections on the very first Draw — bottom status bar, channel-name SeString chunks, window bounds check, hint
banner, autocomplete and input-preview calculation — so the initial ImGui layout cost is spread between frame 0
and frame 1 instead of all hitting at once. At 60 fps the user sees those sections one frame (~17 ms) later, which
is invisible inside the post-reload font-atlas build window. Slash commands `/hellion`, `/hellionView`,
`/hellionSeString` and `/hellionDebugger` are now registered centrally during plugin load so they work before
their target window is opened the first time. The configuration-button entry in Dalamud's plugin manager hangs on
the same path. Three plugin-load profiling logs (auto-translate warm-up, message-store connect, tab filter) stay
on at Information level as a regression tripwire — if a future change pushes the load past 100 ms again, the cost
is right there in `/xllog`. The release also ships a ChatTwo IPC compatibility layer: HellionChat now mirrors
ChatTwo's full IPC surface (`GetChatInputState`, `ChatInputStateChanged`, `Register`, `Unregister`, `Available`,
`Invoke`) under the `ChatTwo.*` namespace in addition to our existing `HellionChat.*` provider gates, so
third-party integrations that historically only subscribe to ChatTwo's IPC (Artisan's and AllaganTools' context-
menu hooks are the practical examples) keep working without requiring a code change on their side. Conflict
detection prevents ChatTwo from loading in parallel with HellionChat, so there is no slot-collision risk at
runtime. Migration v17 stays (no schema bump). Tenth sub-patch of the v1.4.x polish sweep series (as of
2026-05-15).
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
+36
View File
@@ -10,6 +10,42 @@ to the release pages for details.
---
## 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
+30 -4
View File
@@ -10,14 +10,40 @@ the plugin's privacy-first scope during brainstorming.
---
## Next Cycle (v1.4.9)
## Next Cycle (v1.4.10)
**Plugin-Load Render Polish.** Erststart-Frame-Hitch (~110 ms UiBuilder) and the related Font-Atlas + Auto-Translate
warmup costs surface every load and are reproducible in `/xlstats`. The cycle also unblocks the lazy-window refactor
sketched in `feedback_lazy_window_dalamud` and the slash-command centralisation that comes with it.
**Render Clipper, Symbol-Picker and Final-Cleanup.** Reserve items inherited from the v1.4.9 plan that did not need to
land in the HITCH-cut: an `ImGuiListClipper` for variable-height messages in `DrawMessages` (the OtterGui `ImGuiClip.cs`
wrapper is the idiom anchor), a Symbol Picker popup for the chat input (`imgui_demo.cpp` Popups & Modal Windows section
is the pattern reference), plus the carry-over from v1.4.9: structural First-Frame-Layout rewrite if the v1.4.9 selective
defers turn out to be too narrow once user-side regressions surface. Lazy-Window-Init naive is **not** in scope — the
v1.4.9 Stage-2 diagnose falsified that path (`WindowSystem.windows` is non-thread-safe, Game-Freeze under reload stress,
no measurable HITCH delta). A clean DI-container adoption (Lightless `PluginHostFactory` pattern) belongs in v1.5.x and
will revisit the question with the right threading model.
---
## 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
+6 -6
View File
File diff suppressed because one or more lines are too long