Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3be4e73c27 | |||
| 667950c98e | |||
| 3e91177833 | |||
| 51f18e46a0 | |||
| f66316161b | |||
| 679b8f0f5e | |||
| 0e470fcdce | |||
| abbbf95002 | |||
| fbbbeebade | |||
| 7c9b90c767 | |||
| b81894b859 | |||
| 655c903cb5 | |||
| 8c4afaac17 | |||
| c6a3780753 | |||
| d9f6704316 | |||
| 011490368b | |||
| 8ed10a536b | |||
| 6051e49307 | |||
| 55120e6572 | |||
| 7542d48983 | |||
| 7b36763359 | |||
| eecedd9f97 | |||
| 1003a88cad | |||
| 299fd59cbb | |||
| 74bcb91b65 | |||
| 2c64aaa251 | |||
| 607d2c7241 | |||
| b2a0f3a77c | |||
| d26c4701fa | |||
| 7f317a2b18 | |||
| 38149059c3 | |||
| 67175419a9 |
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+639
-241
File diff suppressed because it is too large
Load Diff
+281
-12
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://github.com/goatcorp/Dalamud)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](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:
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user