Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2a2daf39d | |||
| 7d87f1c4fe | |||
| fe84fd558e | |||
| 624ad20404 | |||
| 54ff88d6d4 | |||
| c955f30422 | |||
| 7a1bd1babc | |||
| d0be75e79d | |||
| e0ead86616 | |||
| b66005daea | |||
| 0fe66d2c3c | |||
| 169168cea9 | |||
| f6d3794d87 | |||
| 763f5a3f5d | |||
| 8a18f7caaa | |||
| 5f7bfb5890 | |||
| 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 | |||
| d3fdcdf43d | |||
| f4ea460644 | |||
| d5735d8dcc | |||
| 80b48ac3ad | |||
| cddd29a986 | |||
| 799fdb67cc | |||
| 69fa0fecbd | |||
| fd5f970a8b | |||
| fee2459e73 | |||
| 63cad62c89 | |||
| dca5de4085 | |||
| 8edc3c70d3 | |||
| 3c33acf6d7 | |||
| c8ba8c1cd0 | |||
| 94e4828aeb |
@@ -120,17 +120,37 @@ jobs:
|
|||||||
$enBlock = $rest.TrimEnd()
|
$enBlock = $rest.TrimEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
# ---------- Embed-Felder + Per-Field-Caps (Discord-Hard-Limits) ----------
|
||||||
|
# Discord enforces per-embed-field limits separately from the
|
||||||
|
# combined-total limit. We split the DE and EN blocks into two
|
||||||
|
# embeds that share the same release URL so Discord stitches
|
||||||
|
# them into one visual card. Hard caps per Discord docs:
|
||||||
|
# description: 4096 per embed
|
||||||
|
# title: 256 per embed
|
||||||
|
# footer.text: 2048 per embed
|
||||||
|
# combined sum across all embeds: 6000
|
||||||
$title = "Hellion Chat $version — $subtitle"
|
$title = "Hellion Chat $version — $subtitle"
|
||||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
$deDesc = "**Deutsch**`n`n$deBody"
|
||||||
|
$enDesc = "**English**`n`n$enBlock"
|
||||||
$footerText = "Hellion Forge · $versionsnatur"
|
$footerText = "Hellion Forge · $versionsnatur"
|
||||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
$releaseUrl = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||||
if ($totalChars -gt 5500) {
|
|
||||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
|
||||||
}
|
|
||||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
|
||||||
|
|
||||||
# ---------- Embed-Payload bauen ----------
|
if ($deDesc.Length -gt 4096) {
|
||||||
|
throw "V6a: DE-Body too long for one embed ($($deDesc.Length) chars, max 4096). Trim .github/forge-posts/$tag.md or post the announcement manually (see forge style §8)."
|
||||||
|
}
|
||||||
|
if ($enDesc.Length -gt 4096) {
|
||||||
|
throw "V6b: EN-Block too long for one embed ($($enDesc.Length) chars, max 4096). Trim the changelog entry in HellionChat/HellionChat.yaml or post manually."
|
||||||
|
}
|
||||||
|
$totalChars = $title.Length + $deDesc.Length + $enDesc.Length + $footerText.Length
|
||||||
|
if ($totalChars -gt 6000) {
|
||||||
|
throw "V6c: Combined embed chars $totalChars exceed Discord's 6000-total limit. Major-Release detected — post manually via Bot/Multi-Embed (see forge style §8)."
|
||||||
|
}
|
||||||
|
Write-Host "Embed-Caps OK: de=$($deDesc.Length)/4096, en=$($enDesc.Length)/4096, total=$totalChars/6000"
|
||||||
|
|
||||||
|
# ---------- Embed-Payload bauen (zwei Embeds, gleiche url) ----------
|
||||||
|
# Sharing the same `url` tells Discord to render both embeds as a
|
||||||
|
# single contiguous card block. The title sits on the first embed,
|
||||||
|
# the footer + timestamp on the last so it reads as one post.
|
||||||
$payload = [ordered]@{
|
$payload = [ordered]@{
|
||||||
username = "Forge Herald"
|
username = "Forge Herald"
|
||||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||||
@@ -142,9 +162,14 @@ jobs:
|
|||||||
embeds = @(
|
embeds = @(
|
||||||
[ordered]@{
|
[ordered]@{
|
||||||
title = $title
|
title = $title
|
||||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
url = $releaseUrl
|
||||||
color = 12730636
|
color = 12730636
|
||||||
description = $description
|
description = $deDesc
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
url = $releaseUrl
|
||||||
|
color = 12730636
|
||||||
|
description = $enDesc
|
||||||
footer = [ordered]@{ text = $footerText }
|
footer = [ordered]@{ text = $footerText }
|
||||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -3,16 +3,31 @@ subtitle: Code Hygiene and Refactor
|
|||||||
versionsnatur: Maintenance-Cycle
|
versionsnatur: Maintenance-Cycle
|
||||||
---
|
---
|
||||||
|
|
||||||
Wartungs-Patch ohne User-sichtbare Änderungen. Saubere Code-Basis als Vorbereitung auf das v1.4.7-Backlog-Cleanup, plus zwei geerbte Bugfixes aus dem ChatTwo-Upstream `f35b7d3`.
|
Wartungs-Patch ohne User-sichtbare Änderungen. Saubere Code-Basis als Vorbereitung auf das v1.4.7-Backlog-Cleanup, plus
|
||||||
|
zwei geerbte Bugfixes aus dem ChatTwo-Upstream `f35b7d3`.
|
||||||
|
|
||||||
- **preflight.sh härter**: csharpier-Reflow-Check (Block E) und markdownlint (Block F) laufen jetzt im Pre-Push-Gate, statt erst beim Pre-Merge-Review aufzufallen.
|
- **preflight.sh härter**: csharpier-Reflow-Check (Block E) und markdownlint (Block F) laufen jetzt im Pre-Push-Gate,
|
||||||
- **FontManager-Fallback robuster**: Atlas-Toolkit-Throws aus kaputten Font-Configs (IO, InvalidOperation, ArgumentException) fallen jetzt zuverlässig auf NotoSansCjkRegular, statt den Atlas-Build mitzureißen. Der Exception-Typ wird im Log mitgegeben für die Diagnose.
|
statt erst beim Pre-Merge-Review aufzufallen.
|
||||||
- **URL-Validation beim Plugin-Load**: BrandingLinks (5 URLs) und IntegrationLinks (2 URLs) werden via `[ModuleInitializer]` geprüft. Ein Tippfehler bei einer künftigen URL-Rotation wirft jetzt sofort beim Plugin-Load, statt still beim Klick zu scheitern.
|
- **FontManager-Fallback robuster**: Atlas-Toolkit-Throws aus kaputten Font-Configs (IO, InvalidOperation,
|
||||||
- **Cherry-Pick aus ChatTwo `f35b7d3`** — Memory-Leak in `Chat.SetChannel`: der native `Utf8String` wird jetzt auch dann freigegeben, wenn der Linkshell-Check den Channel ablehnt (vorher gefangen im early-return).
|
ArgumentException) fallen jetzt zuverlässig auf NotoSansCjkRegular, statt den Atlas-Build mitzureißen. Der
|
||||||
- **Cherry-Pick aus ChatTwo `f35b7d3`** — `Tab.Clone()` Deep-cloned jetzt `UsedChannel` und `TellTarget`. Vorher Reference-Share-Bug: PopOut- und Temp-Tabs mutierten sich gegenseitig.
|
Exception-Typ wird im Log mitgegeben für die Diagnose.
|
||||||
- **Aktive-Tab-Underline pixel-perfect bei DPI-Scaling**: Die Underline-Pill skaliert jetzt mit `ImGuiHelpers.GlobalScale` und rundet die DrawList-Koordinaten auf physische Pixel. Kein Sub-Pixel-Blur mehr auf 125/150%-Setups.
|
- **URL-Validation beim Plugin-Load**: BrandingLinks (5 URLs) und IntegrationLinks (2 URLs) werden via
|
||||||
- **IconButton-Width-Fix**: der manuelle `width - 2 * CellPadding.X`-Subtract verlor den HUD-Scale (Padding skaliert, der raw int nicht). Gemessene Breite läuft jetzt unverändert durch.
|
`[ModuleInitializer]` geprüft. Ein Tippfehler bei einer künftigen URL-Rotation wirft jetzt sofort beim Plugin-Load,
|
||||||
- **Test-Isolation für MessageStore**: `Dalamud.Utility.Util`-Surface (IsWine, OpenLink) läuft jetzt durch eine `IPlatformUtil`-Indirektion. MessageStores `IsWine`-Probe ist isoliert testbar in der Build-Suite. Plus: HellionStyle-ChildBgAlpha als Pure-Helper extrahiert, Plugin.SaveConfig kopiert nur Session-Tabs statt der ganzen Tab-Liste, SettingsOverview cached den DrawList einmal pro Frame.
|
statt still beim Klick zu scheitern.
|
||||||
- **Built-in-Theme-Roster**: Crystal Nocturne (Royal Sapphire + Electric Magenta auf Obsidian, von CRYSTALLITE) ersetzt Moonlit Bloom. User mit Moonlit Bloom als aktivem Theme fallen beim ersten Plugin-Load auf Hellion Arctic zurück.
|
- **Cherry-Pick aus ChatTwo `f35b7d3`** — Memory-Leak in `Chat.SetChannel`: der native `Utf8String` wird jetzt auch dann
|
||||||
|
freigegeben, wenn der Linkshell-Check den Channel ablehnt (vorher gefangen im early-return).
|
||||||
|
- **Cherry-Pick aus ChatTwo `f35b7d3`** — `Tab.Clone()` Deep-cloned jetzt `UsedChannel` und `TellTarget`. Vorher
|
||||||
|
Reference-Share-Bug: PopOut- und Temp-Tabs mutierten sich gegenseitig.
|
||||||
|
- **Aktive-Tab-Underline pixel-perfect bei DPI-Scaling**: Die Underline-Pill skaliert jetzt mit
|
||||||
|
`ImGuiHelpers.GlobalScale` und rundet die DrawList-Koordinaten auf physische Pixel. Kein Sub-Pixel-Blur mehr auf
|
||||||
|
125/150%-Setups.
|
||||||
|
- **IconButton-Width-Fix**: der manuelle `width - 2 * CellPadding.X`-Subtract verlor den HUD-Scale (Padding skaliert,
|
||||||
|
der raw int nicht). Gemessene Breite läuft jetzt unverändert durch.
|
||||||
|
- **Test-Isolation für MessageStore**: `Dalamud.Utility.Util`-Surface (IsWine, OpenLink) läuft jetzt durch eine
|
||||||
|
`IPlatformUtil`-Indirektion. MessageStores `IsWine`-Probe ist isoliert testbar in der Build-Suite. Plus:
|
||||||
|
HellionStyle-ChildBgAlpha als Pure-Helper extrahiert, Plugin.SaveConfig kopiert nur Session-Tabs statt der ganzen
|
||||||
|
Tab-Liste, SettingsOverview cached den DrawList einmal pro Frame.
|
||||||
|
- **Built-in-Theme-Roster**: Crystal Nocturne (Royal Sapphire + Electric Magenta auf Obsidian, von CRYSTALLITE) ersetzt
|
||||||
|
Moonlit Bloom. User mit Moonlit Bloom als aktivem Theme fallen beim ersten Plugin-Load auf Hellion Arctic zurück.
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
subtitle: Backlog Cleanup and Mid-Features
|
||||||
|
versionsnatur: Mid-Feature-Patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Achter Sub-Patch der v1.4.x Polish-Sweep-Serie. Erstes User-sichtbares Feature-Bundle seit v1.4.5 — angepinnte Tell-Tabs
|
||||||
|
die Relog überleben, opt-in Honorific-Glow, plus eine konfigurierbare Sidebar.
|
||||||
|
|
||||||
|
- **TempTell anpinnen**: Rechtsklick auf einen TempTell-Tab in der Sidebar → „Tab anpinnen". Angepinnte Tabs überleben
|
||||||
|
Plugin-Reload und Char-Logout, behalten ihre Konversations-Historie (wird beim Rehydrate aus dem MessageStore
|
||||||
|
nachgeladen) und bleiben an die gleiche /tell-Person gebunden. Hard-Cap 5 angepinnte Tabs in einem separaten Pool —
|
||||||
|
die normalen Auto-Tell-Tabs (15er Cap) sind davon entkoppelt, Gesamt-Decke 20. Die Sidebar gruppiert angepinnte Tabs
|
||||||
|
in einer eigenen „Angepinnt"-Sektion mit eigenem Trenner.
|
||||||
|
- **Honorific Glow-Outline**: rendert jetzt eine 8-Richtungs-DrawList-Outline wenn der Honorific-Titel eine Glow-Farbe
|
||||||
|
trägt. Opt-in via **Settings → Integrationen → Glow-Outline rendern (Honorific)** (Default OFF). Gradient (Color3 /
|
||||||
|
GradientColourSet / Wave / Pulse) wird geparst und im DTO weitergereicht, rendert aktuell aber statisch als
|
||||||
|
Primärfarbe — der volle Gradient-Port (Animations-Algorithmus + Pride-Palette) kommt als eigener Cycle nach.
|
||||||
|
- **Sidebar-Breite konfigurierbar**: in **Theme & Layout** ein Slider 44–160 px. Default bleibt 44 px (icon-only), aber
|
||||||
|
breiter machen damit Sektion-Header wie „Aktive Tells (3)" oder „Angepinnt (2)" nicht abgeschnitten werden.
|
||||||
|
- **Settings-Save Channel-Fix**: ein Save mit aktivem Party- oder Linkshell-Tab konnte den Chat-Input zurück auf
|
||||||
|
`/tell <angepinnte Person>` springen lassen. `Configuration.UpdateFrom` bewahrt jetzt den Runtime-`CurrentChannel`
|
||||||
|
über den persistent-Tab-Merge hinweg, und `TabSwitched` deep-cloned den Seed-Channel statt sich den `UsedChannel` mit
|
||||||
|
dem vorigen Tab zu teilen.
|
||||||
|
- **Internal**: `IPluginLogProxy`-Indirektion vor Dalamud's `IPluginLog` über alle ~91 `Plugin.Log`-Call-Sites. Damit
|
||||||
|
läuft `MessageStore.Migrate0` voll-isoliert in xUnit (F12.1-Lücke aus v1.4.6 geschlossen). Plus: TempTab-Counter als
|
||||||
|
abgeleitete Property statt gecachtes Interlocked-Feld — die neuen Pin/Unpin-Übergänge sind Cold-Path, kein
|
||||||
|
Lock-Free-Vorteil mehr. Migration v16 → v17 ist rein additiv (neues `Tab.IsPinned`-Bool, Default false).
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
subtitle: DI Foundation und Service-Refactor
|
||||||
|
versionsnatur: Architektur-Cycle
|
||||||
|
---
|
||||||
|
|
||||||
|
- **Architektur-Umbau ohne User-spürbare Verhaltens-Änderung:** der
|
||||||
|
Plugin-Bootstrap wechselt auf einen Generic-Host DI-Container
|
||||||
|
(`Microsoft.Extensions.Hosting` + `IServiceCollection`) nach dem
|
||||||
|
Lightless-Sync-Muster. 18 Service-Klassen wandern von einem
|
||||||
|
statischen `Plugin.LogProxy`-Locator auf typisierte
|
||||||
|
`ILogger<T>`-Constructor-Injection. `DalamudLogger` brückt
|
||||||
|
`Microsoft.Extensions.Logging` über auf Dalamuds `IPluginLog` —
|
||||||
|
im xllog erscheinen jetzt Service-spezifische Spalten wie
|
||||||
|
`[ MessageManager]` und `[Honori...ervice]`.
|
||||||
|
- **Plugin.LogProxy bleibt für die acht Buckets erhalten,** die
|
||||||
|
Constructor-Injection nicht erreicht: Static-Helper (EmoteCache,
|
||||||
|
AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-Reflektion
|
||||||
|
(Configuration), Data-Class mit Massen-Instanziierung (Message)
|
||||||
|
und Instanz-Klassen die nur aus Static-Methods loggen (FontManager,
|
||||||
|
eine GameFunctions-Stelle).
|
||||||
|
- **Performance bestätigt durch Cross-Plugin-Baseline:** HellionChat
|
||||||
|
First-Frame-HITCH 77 ms Median, Chat 2 v1.40.2 74 ms Median — kein
|
||||||
|
DI-Penalty gegenüber dem Upstream-Fork-Origin. Lightless und
|
||||||
|
XIVInstantMessenger liegen bei ~7 ms weil sie ihren FontAtlas-Build
|
||||||
|
deferren; das wird das v1.5.1-Item.
|
||||||
|
- **User-sichtbarer Bug-Fix nebenbei:** Slash-Command-Einfügen in das
|
||||||
|
Chat-Eingabefeld (Friend-List "/tell"-Action plus Plugin-Inserts
|
||||||
|
von Artisan, AllaganTools und ähnlichen) ersetzt jetzt den
|
||||||
|
vorhandenen Input, statt anzukonkatenieren. Cherry-Pick aus ChatTwo
|
||||||
|
upstream `ee7768ac` mit Namespace-Anpassung.
|
||||||
|
- **Foundation für die Plugin-Integrations-Wave:** v1.5.7-11
|
||||||
|
(Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM
|
||||||
|
Quick-DM) werden ab jetzt strukturell handhabbar — neue Services
|
||||||
|
sind ein `services.AddSingleton<T>` plus ein paar Factory-Lambda-
|
||||||
|
Zeilen, kein Plugin.cs-Anflanschen mehr.
|
||||||
|
- Migration v17 unverändert: kein Schema-Bump, kein
|
||||||
|
Config-Migrations-Aufwand.
|
||||||
@@ -4,10 +4,12 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions.Types;
|
using HellionChat.GameFunctions.Types;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -18,26 +20,38 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
private readonly Plugin _plugin;
|
private readonly Plugin _plugin;
|
||||||
private readonly MessageManager _messageManager;
|
private readonly MessageManager _messageManager;
|
||||||
private readonly MessageStore _store;
|
private readonly MessageStore _store;
|
||||||
|
private readonly ILogger<AutoTellTabsService> _logger;
|
||||||
private readonly object _tempTabsLock = new();
|
private readonly object _tempTabsLock = new();
|
||||||
|
|
||||||
// F2.1: lock-free counter mirrors Config.Tabs.Count(IsTempTab) so the
|
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
|
||||||
// hot-path getter doesn't contend with HandleTell on every render frame.
|
// of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
|
||||||
// Bumped from inside the existing mutation paths so it stays consistent
|
// in their own bucket. A configurable cap is a vault-backlog anchor for
|
||||||
// with the underlying list — see SpawnTempTab, DropOldestTempTab, OnLogout
|
// a later cycle if tester feedback demands it.
|
||||||
// and ResyncTempTabCounter (used by Plugin.cs snapshot-restore).
|
internal const int MaxPinnedTempTabs = 5;
|
||||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
|
||||||
private int _activeTempTabCount;
|
|
||||||
|
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
|
|
||||||
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
|
internal AutoTellTabsService(
|
||||||
|
Plugin plugin,
|
||||||
|
MessageManager messageManager,
|
||||||
|
MessageStore store,
|
||||||
|
ILogger<AutoTellTabsService> logger
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_plugin = plugin;
|
_plugin = plugin;
|
||||||
_messageManager = messageManager;
|
_messageManager = messageManager;
|
||||||
_store = store;
|
_store = store;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int ActiveTempTabCount => Volatile.Read(ref _activeTempTabCount);
|
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||||
|
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||||
|
// Replaces the F2.1 Interlocked counter because the new pin-state
|
||||||
|
// transitions are cold-path and don't need lock-free reads.
|
||||||
|
internal int ActiveTempTabCount =>
|
||||||
|
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||||
|
|
||||||
|
internal int PinnedTempTabCount => Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||||
|
|
||||||
internal void Initialize()
|
internal void Initialize()
|
||||||
{
|
{
|
||||||
@@ -46,23 +60,51 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed the counter from the persisted Tabs list so a config that already
|
// Pinned tabs come out of the JSON with TellTarget set but
|
||||||
// contains TempTabs from a prior session starts in sync. Plugin.cs:168
|
// CurrentChannel reset (NonSerialized). Without re-seeding, the chat
|
||||||
// crash-recovery has already dropped TempTabs by the time we get here,
|
// input has no tell-target on the active pinned tab, and the
|
||||||
// so the snapshot reflects post-recovery reality.
|
// game-side channel hook only repaints CurrentChannel once the user
|
||||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
// triggers a /tell or channel switch.
|
||||||
|
RehydratePinnedTabs();
|
||||||
|
|
||||||
_messageManager.MessageProcessed += HandleTell;
|
_messageManager.MessageProcessed += HandleTell;
|
||||||
Plugin.ClientState.Logout += OnLogout;
|
Plugin.ClientState.Logout += OnLogout;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// F2.1: callable from outside paths that mutate Config.Tabs directly
|
private void RehydratePinnedTabs()
|
||||||
// (Plugin.cs snapshot-restore). Atomically re-pegs the counter to the
|
|
||||||
// live IsTempTab count.
|
|
||||||
internal void ResyncTempTabCounter()
|
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||||
|
_logger.LogDebug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found");
|
||||||
|
|
||||||
|
foreach (var tab in Plugin.Config.Tabs)
|
||||||
|
{
|
||||||
|
if (!TabLifecycleHelpers.IsInPinnedPool(tab))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (tab.TellTarget is null || !tab.TellTarget.IsSet())
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
$"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget "
|
||||||
|
+ $"(Name={tab.TellTarget?.Name ?? "<null>"} World={tab.TellTarget?.World ?? 0}). "
|
||||||
|
+ "Chat input on this tab will be empty until the partner sends a tell or you /tell manually."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.Channel ??= InputChannel.Tell;
|
||||||
|
tab.CurrentChannel.Channel = InputChannel.Tell;
|
||||||
|
tab.CurrentChannel.TellTarget = tab.TellTarget.Clone();
|
||||||
|
|
||||||
|
// MessageList is NonSerialized so pinned tabs come back empty.
|
||||||
|
// Preload the same history window the spawn path uses so the user
|
||||||
|
// sees the recent conversation, not a blank tab.
|
||||||
|
PreloadHistory(tab, tab.TellTarget.Name, tab.TellTarget.World, Guid.Empty);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -96,7 +138,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
if (partner == null)
|
if (partner == null)
|
||||||
{
|
{
|
||||||
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
||||||
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
||||||
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
||||||
@@ -110,7 +152,23 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
// Already routed via MessageManager pipeline
|
// Already routed via MessageManager pipeline. Repair the
|
||||||
|
// tell-target if the fallback hit a pinned tab whose
|
||||||
|
// TellTarget didn't survive a previous round-trip — keeps
|
||||||
|
// FindTempTab fast on the next message.
|
||||||
|
if (
|
||||||
|
existing.IsPinned
|
||||||
|
&& (existing.TellTarget is null || !existing.TellTarget.IsSet())
|
||||||
|
)
|
||||||
|
{
|
||||||
|
existing.TellTarget = new TellTarget(
|
||||||
|
partner.Value.Name,
|
||||||
|
partner.Value.World,
|
||||||
|
0,
|
||||||
|
TellReason.Direct
|
||||||
|
);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,22 +218,35 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tab? FindTempTab(string name, uint world)
|
private static Tab? FindTempTab(string name, uint world)
|
||||||
{
|
{
|
||||||
return Plugin.Config.Tabs.FirstOrDefault(t =>
|
var byTarget = Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||||
t.IsTempTab
|
t.IsTempTab
|
||||||
&& t.TellTarget != null
|
&& t.TellTarget != null
|
||||||
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
||||||
&& t.TellTarget.World == world
|
&& t.TellTarget.World == world
|
||||||
);
|
);
|
||||||
|
if (byTarget != null)
|
||||||
|
return byTarget;
|
||||||
|
|
||||||
|
// Fallback: match by tab name. Pinned tabs are named via
|
||||||
|
// FormatTabName(player, world) at spawn time, so the name is a
|
||||||
|
// stable secondary key when TellTarget didn't survive a save/load
|
||||||
|
// (older configs from a renamed pin, malformed migrations, etc.).
|
||||||
|
var expectedName = FormatTabName(name, world);
|
||||||
|
return Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||||
|
t.IsTempTab && string.Equals(t.Name, expectedName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DropOldestTempTab()
|
internal void DropOldestTempTab()
|
||||||
{
|
{
|
||||||
// Prioritize greeted tabs for drop; within each bucket, drop by oldest LastActivity
|
// Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
|
||||||
|
// never drop candidates. They leave the bucket only via Unpin or
|
||||||
|
// PromoteToPermanent.
|
||||||
var victim = Plugin
|
var victim = Plugin
|
||||||
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||||
.Where(t => t.Tab.IsTempTab)
|
.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
|
||||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||||
.ThenBy(t => t.Tab.LastActivity)
|
.ThenBy(t => t.Tab.LastActivity)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -198,7 +269,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||||
Interlocked.Decrement(ref _activeTempTabCount);
|
|
||||||
|
|
||||||
// Re-anchor active tab to avoid silent switch when tab is dropped
|
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||||
if (victim.Index <= _plugin.LastTab)
|
if (victim.Index <= _plugin.LastTab)
|
||||||
@@ -223,7 +293,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.Add(tab);
|
Plugin.Config.Tabs.Add(tab);
|
||||||
Interlocked.Increment(ref _activeTempTabCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
||||||
@@ -300,7 +369,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
||||||
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
|
_logger.LogError(ex, "[AutoTellTabs] History preload failed");
|
||||||
tab.Messages.AddPrune(
|
tab.Messages.AddPrune(
|
||||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||||
MessageManager.MessageDisplayLimit
|
MessageManager.MessageDisplayLimit
|
||||||
@@ -354,14 +423,16 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
{
|
{
|
||||||
lock (_tempTabsLock)
|
lock (_tempTabsLock)
|
||||||
{
|
{
|
||||||
// Snapshot active tab index before mutating list
|
// Pinned TempTabs must survive char-switch — that's the whole point
|
||||||
|
// of pinning. Only unpinned ones get stripped.
|
||||||
var lastIndex = _plugin.LastTab;
|
var lastIndex = _plugin.LastTab;
|
||||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||||
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
|
var currentWasUnpinnedTempTab =
|
||||||
|
lastIndexValid
|
||||||
|
&& TabLifecycleHelpers.IsInUnpinnedPool(Plugin.Config.Tabs[lastIndex]);
|
||||||
|
|
||||||
// Clean up pop-out windows before removing temp tabs
|
|
||||||
var poppedTempTabIds = Plugin
|
var poppedTempTabIds = Plugin
|
||||||
.Config.Tabs.Where(t => t.IsTempTab && t.PopOut)
|
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
|
||||||
.Select(t => t.Identifier)
|
.Select(t => t.Identifier)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (poppedTempTabIds.Count > 0)
|
if (poppedTempTabIds.Count > 0)
|
||||||
@@ -377,15 +448,76 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var removed = Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||||
Interlocked.Add(ref _activeTempTabCount, -removed);
|
|
||||||
|
|
||||||
// Force switch to tab 0 if active tab was temp or index is now out of range
|
// Force switch to tab 0 if active tab was an unpinned temp tab or
|
||||||
|
// index is now out of range. Pinned tabs survive — no switch needed.
|
||||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||||
if (currentWasTempTab || !stillValid)
|
if (currentWasUnpinnedTempTab || !stillValid)
|
||||||
{
|
{
|
||||||
_plugin.WantedTab = 0;
|
_plugin.WantedTab = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool TryPin(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsTempTab || tab.IsPinned)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinnedTempTabCount >= MaxPinnedTempTabs)
|
||||||
|
{
|
||||||
|
WrapperUtil.AddNotification(
|
||||||
|
string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
|
||||||
|
NotificationType.Warning
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsPinned = true;
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
|
||||||
|
);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Unpin(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsPinned)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the unpinned pool is already full, dropping the oldest before
|
||||||
|
// flipping the flag avoids counting the just-unpinned tab as a drop
|
||||||
|
// candidate.
|
||||||
|
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||||
|
{
|
||||||
|
DropOldestTempTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsPinned = false;
|
||||||
|
_logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void PromoteToPermanent(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsTempTab)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsTempTab = false;
|
||||||
|
tab.IsPinned = false;
|
||||||
|
tab.TellTarget = TellTarget.Empty();
|
||||||
|
_logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)");
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
internal sealed class Commands : IDisposable
|
internal sealed class Commands : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, CommandWrapper> Registered = [];
|
private readonly Dictionary<string, CommandWrapper> Registered = [];
|
||||||
|
private readonly ILogger<Commands> _logger;
|
||||||
|
|
||||||
|
public Commands(ILogger<Commands> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -52,7 +59,7 @@ internal sealed class Commands : IDisposable
|
|||||||
{
|
{
|
||||||
if (!Registered.TryGetValue(command, out var wrapper))
|
if (!Registered.TryGetValue(command, out var wrapper))
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning($"Missing registration for command {command}");
|
_logger.LogWarning($"Missing registration for command {command}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +69,7 @@ internal sealed class Commands : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, $"Error while executing command {command}");
|
_logger.LogError(ex, $"Error while executing command {command}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 16;
|
private const int LatestVersion = 17;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
// silently, like before.
|
// silently, like before.
|
||||||
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
Plugin.LogProxy.Warning(
|
||||||
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
||||||
type,
|
type,
|
||||||
PrivacyPersistUnknownChannels
|
PrivacyPersistUnknownChannels
|
||||||
@@ -102,10 +102,22 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool FirstRunCompleted;
|
public bool FirstRunCompleted;
|
||||||
public bool UseHellionFont = true;
|
public bool UseHellionFont = true;
|
||||||
public bool ShowHonorificTitleInHeader = true;
|
public bool ShowHonorificTitleInHeader = true;
|
||||||
|
|
||||||
|
// v1.4.7 opt-in: renders the Honorific glow outline when the title carries
|
||||||
|
// a Glow colour. Default OFF — keeps v1.4.6 visuals untouched for users
|
||||||
|
// who don't care, and dodges the per-frame DrawList overhead on low-end
|
||||||
|
// hardware. Gradient (Color3 / GradientColourSet) is parsed but rendered
|
||||||
|
// as the primary Color until a later cycle ports the animation.
|
||||||
|
public bool ShowHonorificGlow;
|
||||||
public bool EnableAutoTellTabs = true;
|
public bool EnableAutoTellTabs = true;
|
||||||
public int AutoTellTabsLimit = 15;
|
public int AutoTellTabsLimit = 15;
|
||||||
public bool AutoTellTabsCompactDisplay;
|
public bool AutoTellTabsCompactDisplay;
|
||||||
public int AutoTellTabsHistoryPreload = 20;
|
public int AutoTellTabsHistoryPreload = 20;
|
||||||
|
|
||||||
|
// Sidebar width in pixels. Default 44 mirrors the icon-only layout from
|
||||||
|
// v1.2.0; users can widen up to 160 to fit a section-header line like
|
||||||
|
// "Active Tells (3)" without truncation.
|
||||||
|
public int SidebarWidth = 44;
|
||||||
public bool AutoTellTabsShowGreetedToggle;
|
public bool AutoTellTabsShowGreetedToggle;
|
||||||
public bool SeenPopOutInputHint;
|
public bool SeenPopOutInputHint;
|
||||||
public bool PopOutInputEnabled = true;
|
public bool PopOutInputEnabled = true;
|
||||||
@@ -164,6 +176,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool SortAutoTranslate;
|
public bool SortAutoTranslate;
|
||||||
public bool CollapseDuplicateMessages;
|
public bool CollapseDuplicateMessages;
|
||||||
public bool CollapseKeepUniqueLinks;
|
public bool CollapseKeepUniqueLinks;
|
||||||
|
public bool SymbolPickerEnabled = true;
|
||||||
public bool PlaySounds = true;
|
public bool PlaySounds = true;
|
||||||
public bool KeepInputFocus = true;
|
public bool KeepInputFocus = true;
|
||||||
public int MaxLinesToRender = 2_500; // 1-10000
|
public int MaxLinesToRender = 2_500; // 1-10000
|
||||||
@@ -258,6 +271,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
SortAutoTranslate = other.SortAutoTranslate;
|
SortAutoTranslate = other.SortAutoTranslate;
|
||||||
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
||||||
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
||||||
|
SymbolPickerEnabled = other.SymbolPickerEnabled;
|
||||||
PlaySounds = other.PlaySounds;
|
PlaySounds = other.PlaySounds;
|
||||||
KeepInputFocus = other.KeepInputFocus;
|
KeepInputFocus = other.KeepInputFocus;
|
||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
@@ -278,16 +292,20 @@ public class Configuration : IPluginConfiguration
|
|||||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||||
|
|
||||||
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||||
// not destroy open tell conversations. For persistent tabs, capture
|
// not destroy open tell conversations. Pinned TempTabs are persistent
|
||||||
// the live MessageList and LastSendUnread by Identifier before the
|
// and come through `other` like regular tabs; unpinned TempTabs are
|
||||||
// replace and restore them onto the freshly cloned tabs; new tabs
|
// session-only and held from the local state. For persistent tabs
|
||||||
// get an empty MessageList, deleted tabs lose their history (intended).
|
// (incl. pinned), capture live runtime state by Identifier and restore
|
||||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
// it onto the freshly cloned tabs — CurrentChannel is critical because
|
||||||
var livePersistentSession = Tabs.Where(t => !t.IsTempTab)
|
// the user may have switched channel in-game between settings-open
|
||||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
|
// and settings-save, and we'd otherwise overwrite that with the
|
||||||
|
// settings-time snapshot.
|
||||||
|
var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||||
|
var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t))
|
||||||
|
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread, t.CurrentChannel));
|
||||||
|
|
||||||
Tabs = other
|
Tabs = other
|
||||||
.Tabs.Where(t => !t.IsTempTab)
|
.Tabs.Where(t => !t.IsTempTab || t.IsPinned)
|
||||||
.Select(t =>
|
.Select(t =>
|
||||||
{
|
{
|
||||||
var clone = t.Clone();
|
var clone = t.Clone();
|
||||||
@@ -295,11 +313,12 @@ public class Configuration : IPluginConfiguration
|
|||||||
{
|
{
|
||||||
clone.Messages = live.Messages;
|
clone.Messages = live.Messages;
|
||||||
clone.LastSendUnread = live.LastSendUnread;
|
clone.LastSendUnread = live.LastSendUnread;
|
||||||
|
clone.CurrentChannel = live.CurrentChannel;
|
||||||
}
|
}
|
||||||
return clone;
|
return clone;
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
Tabs.AddRange(liveTempTabs);
|
Tabs.AddRange(liveUnpinnedTempTabs);
|
||||||
|
|
||||||
ChatTabForward = other.ChatTabForward;
|
ChatTabForward = other.ChatTabForward;
|
||||||
ChatTabBackward = other.ChatTabBackward;
|
ChatTabBackward = other.ChatTabBackward;
|
||||||
@@ -319,6 +338,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
FirstRunCompleted = other.FirstRunCompleted;
|
FirstRunCompleted = other.FirstRunCompleted;
|
||||||
UseHellionFont = other.UseHellionFont;
|
UseHellionFont = other.UseHellionFont;
|
||||||
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
||||||
|
ShowHonorificGlow = other.ShowHonorificGlow;
|
||||||
|
|
||||||
// v1.1.0 theme engine fields
|
// v1.1.0 theme engine fields
|
||||||
Theme = other.Theme;
|
Theme = other.Theme;
|
||||||
@@ -330,6 +350,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||||||
|
SidebarWidth = other.SidebarWidth;
|
||||||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||||||
|
|
||||||
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||||||
@@ -404,6 +425,11 @@ public class Tab
|
|||||||
public bool HideWhenInactive;
|
public bool HideWhenInactive;
|
||||||
|
|
||||||
public bool IsTempTab;
|
public bool IsTempTab;
|
||||||
|
|
||||||
|
// Pinned TempTabs survive plugin reload and logout — tester feedback from
|
||||||
|
// Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs)
|
||||||
|
// separate from the AutoTellTabsLimit bucket.
|
||||||
|
public bool IsPinned;
|
||||||
public bool AllSenderMessages;
|
public bool AllSenderMessages;
|
||||||
public TellTarget TellTarget = TellTarget.Empty();
|
public TellTarget TellTarget = TellTarget.Empty();
|
||||||
|
|
||||||
@@ -511,6 +537,7 @@ public class Tab
|
|||||||
HideInBattle = HideInBattle,
|
HideInBattle = HideInBattle,
|
||||||
HideWhenInactive = HideWhenInactive,
|
HideWhenInactive = HideWhenInactive,
|
||||||
IsTempTab = IsTempTab,
|
IsTempTab = IsTempTab,
|
||||||
|
IsPinned = IsPinned,
|
||||||
AllSenderMessages = AllSenderMessages,
|
AllSenderMessages = AllSenderMessages,
|
||||||
TellTarget = TellTarget.Clone(),
|
TellTarget = TellTarget.Clone(),
|
||||||
IsGreeted = IsGreeted,
|
IsGreeted = IsGreeted,
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ public static class EmoteCache
|
|||||||
t =>
|
t =>
|
||||||
{
|
{
|
||||||
if (t.IsFaulted)
|
if (t.IsFaulted)
|
||||||
Plugin.Log.Error(t.Exception!, $"EmoteCache load failed for {emoteCode}");
|
Plugin.LogProxy.Error(
|
||||||
|
t.Exception!,
|
||||||
|
$"EmoteCache load failed for {emoteCode}"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
TaskScheduler.Default
|
TaskScheduler.Default
|
||||||
)
|
)
|
||||||
@@ -158,7 +161,7 @@ public static class EmoteCache
|
|||||||
{
|
{
|
||||||
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
||||||
State = LoadingState.Unloaded;
|
State = LoadingState.Unloaded;
|
||||||
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
|
Plugin.LogProxy.Error(ex, "BetterTTV cache wasn't initialized");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +217,7 @@ public static class EmoteCache
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Plugin.Log.Error("Failed to convert");
|
Plugin.LogProxy.Error("Failed to convert");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +307,7 @@ public static class EmoteCache
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +411,7 @@ public static class EmoteCache
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ using Dalamud.Interface.Utility;
|
|||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
|
// Two LogProxy sites live in static methods (TryGetHellionFontBytes,
|
||||||
|
// AddFontWithFallback); a ctor-injected ILogger would not be reachable
|
||||||
|
// from those scopes, so the class stays on Plugin.LogProxy.
|
||||||
public class FontManager
|
public class FontManager
|
||||||
{
|
{
|
||||||
internal IFontHandle Axis = null!;
|
internal IFontHandle Axis = null!;
|
||||||
@@ -58,7 +61,7 @@ public class FontManager
|
|||||||
);
|
);
|
||||||
if (stream is null)
|
if (stream is null)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
Plugin.LogProxy.Warning(
|
||||||
"Hellion font resource missing — falling back to system default font."
|
"Hellion font resource missing — falling back to system default font."
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
@@ -234,10 +237,10 @@ public class FontManager
|
|||||||
or ArgumentException
|
or ArgumentException
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Atlas-toolkit throws span IO and validation failures; routing the
|
// Atlas-toolkit throws span IO and validation failures; routing
|
||||||
// wider set through the fallback keeps a corrupt font config from
|
// the wider set through the fallback keeps a corrupt font config
|
||||||
// taking down the whole atlas build.
|
// from taking down the whole atlas build.
|
||||||
Plugin.Log.Warning(
|
Plugin.LogProxy.Warning(
|
||||||
e,
|
e,
|
||||||
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
||||||
+ "falling back to NotoSansCjkRegular"
|
+ "falling back to NotoSansCjkRegular"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using InteropGenerator.Runtime;
|
using InteropGenerator.Runtime;
|
||||||
using Lumina.Text.ReadOnly;
|
using Lumina.Text.ReadOnly;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
@@ -98,9 +99,12 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
private long LastPlayerNameDisplayTypeRefresh;
|
private long LastPlayerNameDisplayTypeRefresh;
|
||||||
private PlayerNameDisplayType CurrentPlayerNameDisplayType = PlayerNameDisplayType.FullName;
|
private PlayerNameDisplayType CurrentPlayerNameDisplayType = PlayerNameDisplayType.FullName;
|
||||||
|
|
||||||
public Chat(Plugin plugin)
|
private readonly ILogger<Chat> _logger;
|
||||||
|
|
||||||
|
public Chat(Plugin plugin, ILogger<Chat> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||||
|
|
||||||
ChatLogRefreshHook?.Enable();
|
ChatLogRefreshHook?.Enable();
|
||||||
@@ -236,7 +240,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -266,7 +270,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1; // Prevent vanilla chat log from gaining focus
|
return 1; // Prevent vanilla chat log from gaining focus
|
||||||
@@ -299,7 +303,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||||
worldId = agent->TellWorldId;
|
worldId = agent->TellWorldId;
|
||||||
Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}");
|
_logger.LogDebug($"Detected tell target '[redacted]'@{worldId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||||
@@ -358,7 +362,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +412,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +628,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
if (contentId == 0)
|
if (contentId == 0)
|
||||||
{
|
{
|
||||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
|||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using Lumina.Excel;
|
using Lumina.Excel;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
@@ -37,14 +38,20 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
|
private readonly ILogger<GameFunctions> _logger;
|
||||||
internal KeybindManager KeybindManager { get; }
|
internal KeybindManager KeybindManager { get; }
|
||||||
internal Chat Chat { get; }
|
internal Chat Chat { get; }
|
||||||
|
|
||||||
internal GameFunctions(Plugin plugin)
|
internal GameFunctions(
|
||||||
|
Plugin plugin,
|
||||||
|
ILogger<GameFunctions> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
KeybindManager = new KeybindManager(plugin);
|
_logger = logger;
|
||||||
Chat = new Chat(Plugin);
|
KeybindManager = new KeybindManager(plugin, loggerFactory.CreateLogger<KeybindManager>());
|
||||||
|
Chat = new Chat(Plugin, loggerFactory.CreateLogger<Chat>());
|
||||||
|
|
||||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||||
ResolveTextCommandPlaceholderHook?.Enable();
|
ResolveTextCommandPlaceholderHook?.Enable();
|
||||||
@@ -215,7 +222,8 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(e, "Unable to open adventurer plate");
|
// Static method, no instance _logger reachable here.
|
||||||
|
Plugin.LogProxy.Warning(e, "Unable to open adventurer plate");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +263,7 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||||
if (byteCount >= PlaceholderBufferSize)
|
if (byteCount >= PlaceholderBufferSize)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
||||||
);
|
);
|
||||||
ReplacementName = null;
|
ReplacementName = null;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.UI;
|
|||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions.Types;
|
using HellionChat.GameFunctions.Types;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
|
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
@@ -306,9 +307,12 @@ internal unsafe class KeybindManager : IDisposable
|
|||||||
// VirtualKey.OEM_CLEAR,
|
// VirtualKey.OEM_CLEAR,
|
||||||
};
|
};
|
||||||
|
|
||||||
internal KeybindManager(Plugin plugin)
|
private readonly ILogger<KeybindManager> _logger;
|
||||||
|
|
||||||
|
internal KeybindManager(Plugin plugin, ILogger<KeybindManager> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||||
|
|
||||||
// Handle keybinds from the game on every tick.
|
// Handle keybinds from the game on every tick.
|
||||||
@@ -507,7 +511,7 @@ internal unsafe class KeybindManager : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
<Version>1.4.6</Version>
|
<Version>1.5.0</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Use lock file to pin exact versions -->
|
<!-- Use lock file to pin exact versions -->
|
||||||
@@ -15,6 +15,14 @@
|
|||||||
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
||||||
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||||
|
<!-- v1.5.0 DI-container foundation; matches Lightless pin (Hosting 10.0.7) -->
|
||||||
|
<PackageReference
|
||||||
|
Include="Microsoft.Extensions.DependencyInjection"
|
||||||
|
Version="[10.0.7, 11.0.0)"
|
||||||
|
/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="[10.0.7, 11.0.0)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="[10.0.7, 11.0.0)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="[10.0.7, 11.0.0)" />
|
||||||
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
||||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||||
|
|||||||
+142
-75
@@ -35,100 +35,167 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**
|
**v1.5.0 — DI Foundation and Service Refactor (2026-05-17)**
|
||||||
|
|
||||||
Maintenance patch. No user-visible behaviour changes; tightens the
|
Major architecture cycle. The plugin bootstrap moves to a
|
||||||
development feedback loop, fixes two upstream-inherited bugs, and
|
generic-host DI container (Microsoft.Extensions.Hosting +
|
||||||
prepares the code for the v1.4.7 backlog cleanup.
|
IServiceCollection) modelled on Lightless Sync. Service logging
|
||||||
|
moves from a static Plugin.LogProxy locator to typed
|
||||||
|
Microsoft.Extensions.Logging.ILogger<T> via constructor injection,
|
||||||
|
bridged over Dalamud's IPluginLog by a custom DalamudLogger trio.
|
||||||
|
|
||||||
- preflight.sh gains a csharpier reflow check and a markdownlint
|
What changes under the hood:
|
||||||
pass so style drift and markdown violations are caught at the
|
|
||||||
pre-push gate
|
- 18 instance-class services migrate to ILogger<T> via constructor
|
||||||
- FontManager fallback catches the full set of atlas-toolkit
|
injection across four slices: data layer (MessageStore,
|
||||||
throws (IO, InvalidOperation, ArgumentException) — a corrupt
|
MessageManager, AutoTellTabsService), IPC and integrations
|
||||||
font config no longer takes down the whole atlas build
|
(HonorificService, IpcManager, TypingIpc, ExtraChat, the three
|
||||||
- BrandingLinks and IntegrationLinks URLs validated on plugin
|
GameFunctions classes), UI window layer (ChatLogWindow,
|
||||||
load — a typo in a future URL rotation now throws at startup
|
DbViewer, Popout, three settings tabs), and root (Commands,
|
||||||
- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel
|
ThemeRegistry, PayloadHandler).
|
||||||
no longer leaks the native Utf8String when the linkshell check
|
- Plugin.LogProxy stays in place for the eight buckets ctor
|
||||||
rejects the channel
|
injection cannot reach: static helpers (EmoteCache,
|
||||||
- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now
|
AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-reflected
|
||||||
deep-clones UsedChannel and TellTarget — PopOut and Temp tabs
|
types (Configuration), the Message data class, and instance
|
||||||
no longer mutate each other's channel state
|
classes that only log from static methods (FontManager, one
|
||||||
- Active-tab underline scales with DPI and rounds to physical
|
GameFunctions site).
|
||||||
pixels for crisp rendering above 100% scaling
|
- Plugin.cs finishes at 1012 lines — virtually identical to the
|
||||||
- IconButton width parameter no longer subtracts HUD-scaled
|
pre-cycle 1013. The new Phase-1 host build and Plugin.X bridge
|
||||||
padding from a raw int (measured width passes through verbatim)
|
wiring trade out exactly the service and window allocations
|
||||||
- Internal: HellionStyle ChildBgAlpha extracted to a testable
|
that previously lived in LoadAsync.
|
||||||
helper; Plugin.SaveConfig clones only the temp tabs;
|
- Cross-plugin baseline confirms no performance penalty against
|
||||||
SettingsOverview caches the draw-list per frame;
|
Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2
|
||||||
Dalamud.Utility.Util surface routed through an IPlatformUtil
|
74 ms median. Lightless and XIVInstantMessenger sit around
|
||||||
indirection (MessageStore IsWine probe is now testable in
|
7 ms by deferring their font-atlas build past Finished
|
||||||
isolation)
|
loading — that pattern is the v1.5.1 follow-up.
|
||||||
- Built-in themes: Crystal Nocturne (sapphire and electric
|
|
||||||
magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom.
|
User-visible:
|
||||||
Users with Moonlit Bloom selected fall back to Hellion Arctic
|
|
||||||
on first load
|
- Slash-command insert fix: pasting a slash command into the
|
||||||
|
chat input (Friend List "/tell" action, plugin-driven inserts
|
||||||
|
from Artisan, AllaganTools etc.) now replaces the existing
|
||||||
|
input instead of concatenating. Cherry-picked from ChatTwo
|
||||||
|
upstream ee7768ac with namespace adaptation.
|
||||||
|
|
||||||
|
Migration v17 stays (no schema bump).
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.4.5 — UX and Robustness (2026-05-12)**
|
**v1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)**
|
||||||
|
|
||||||
Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw
|
Eleventh and final sub-patch of the v1.4.x polish-sweep series.
|
||||||
failures surface as a notification, the first-run wizard has an
|
Symbol picker for the chat input, a tell-history reload fix for
|
||||||
explicit "Later" option, the input history clears on plugin reload,
|
users with many active partners, and a closing cleanup sweep
|
||||||
and the status bar version slot stops clipping in narrow windows.
|
before v1.5.0 picks up the DI-container adoption.
|
||||||
|
|
||||||
- Chat window draw errors now show a one-shot notification instead
|
- Symbol picker: a small smile-icon button left of the channel
|
||||||
of failing silently — stack trace stays in /xllog
|
indicator opens a popup with two tabs. The first lists all 161
|
||||||
- First-run wizard: explicit "Later — keep defaults" button.
|
FFXIV PUA glyphs (Dalamud's SeIconChar enum); the second
|
||||||
Closing the X no longer silently accepts the defaults; the wizard
|
carries 97 server-verified BMP symbols (latin marks, currency,
|
||||||
reopens on the next plugin load if nothing was picked
|
the full Greek alphabet, geometric shapes, suits, notes) —
|
||||||
- InputHistoryService clears on plugin dispose so the previous
|
every one of them round-tripped through /echo and /say in a
|
||||||
session's typed commands don't bleed into the next load
|
four-round probe so the in-channel render matches what the
|
||||||
- Status bar hides the version slot when the chat window is too
|
picker shows. Click drops the glyph at the caret, multi-insert
|
||||||
narrow to fit all five slots without overlap
|
keeps the popup open, and a recent-used strip floats the last
|
||||||
- Internal: explicit session-only Auto-Tell-Tab invariant in
|
sixteen picks across both tabs. Toggle in Settings → Chat →
|
||||||
Plugin.cs plus a pinning test in the Build-Suite
|
Message behaviour, default on.
|
||||||
- Internal: FontManager falls back to the system font if the
|
- Pinned auto-tell tabs reload their full history again: a
|
||||||
embedded Hellion font resource is missing — logs a Warning
|
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.4 — Threading and IPC safety polish (2026-05-12)**
|
**v1.4.9 — Plugin-Load Render Polish (2026-05-15)**
|
||||||
|
|
||||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
|
Tenth sub-patch of the v1.4.x polish-sweep series. First-frame
|
||||||
assumptions are documented per-method, a hot-path lock falls
|
render cost drops from ~127 ms median to ~76 ms median,
|
||||||
away, and the privacy filter speaks up when an unknown ChatType
|
comfortably under Dalamud's 100 ms HITCH warning threshold.
|
||||||
shows up.
|
|
||||||
|
|
||||||
- AutoTellTabs hot-path getter uses an Interlocked counter
|
- First-frame defer: six non-essential rendering sections inside
|
||||||
instead of taking the lock on every read
|
ChatLogWindow skip their first Draw and run one frame later
|
||||||
- Honorific integration: per-method threading banners, plus
|
(bottom status bar, channel-name SeString chunks, window bounds
|
||||||
Warning-level log on unsubscribe failure
|
check, v0.6.1 hint banner, autocomplete, input-preview
|
||||||
- AutoTranslate warmup thread marked IsBackground so plugin
|
calculation). User-visible delay is ~17 ms at 60 fps, hidden
|
||||||
unload doesn't wait for it
|
inside the post-reload font-atlas build window.
|
||||||
- PrivacyFilter logs once per unknown ChatType so a future
|
- Slash-command centralisation: /hellion, /hellionView,
|
||||||
patch's added channel doesn't drop off the radar
|
/hellionSeString and /hellionDebugger are registered in
|
||||||
- New installs persist unknown channels by default; existing
|
LoadAsync instead of inside the corresponding window
|
||||||
configs keep their explicit choice
|
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.3 — Faster plugin load + new repo (2026-05-08)**
|
**v1.4.8 — Hook-Layer and Polish Quick-Wins (2026-05-14)**
|
||||||
|
|
||||||
Heavy startup work (migrations, hooks, windows) now runs async so
|
Ninth sub-patch of the v1.4.x polish-sweep series. Hook-layer
|
||||||
Dalamud's UI stays responsive during load. Load time is comparable
|
cluster (DbViewer FTS5 full-text search, ad-block foundation
|
||||||
to v1.4.2 — this is the foundation for v1.4.4 optimisations.
|
investigation) plus three polish quick-wins.
|
||||||
|
|
||||||
- Two-phase async load via IAsyncDalamudPlugin
|
- DbViewer full-text search: optional FTS5 index across the full
|
||||||
- Schema-gate replaces the v9→v16 migration chain; old configs
|
chat history. Built asynchronously on first load after the
|
||||||
require a v1.4.2 install first
|
update with a progress toast. The local page-filter remains
|
||||||
- AutoTranslate cache loads on first use instead of every startup
|
available as the default mode. Queries match as exact phrases
|
||||||
- Custom font (Hellion-Exo2) appears with a brief pop after load
|
-- multi-word terms must appear together in order; advanced
|
||||||
- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using Dalamud.Plugin;
|
||||||
|
using HellionChat.Ipc;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Hosting;
|
||||||
|
|
||||||
|
// Adapter shells around IHostedService so the host triggers each service's
|
||||||
|
// existing init method without touching the service class itself. Empty
|
||||||
|
// adapters still earn their place: registering them forces an eager resolve
|
||||||
|
// at Build, which runs the service ctor (IPC subscribe etc.) right then
|
||||||
|
// instead of lazily on first GetRequiredService.
|
||||||
|
|
||||||
|
internal sealed class FontManagerInitHostedService(FontManager fontManager) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
fontManager.BuildFonts();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ThemeRegistryInitHostedService(ThemeRegistry registry) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Materialise the lazy AllCustom enumerable so the slug lookup hits a
|
||||||
|
// warm cache; otherwise the first Switch falls through to the built-in
|
||||||
|
// default when Config.Theme points at a custom slug.
|
||||||
|
foreach (var _ in registry.AllCustom()) { }
|
||||||
|
registry.Switch(Plugin.Config.Theme);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC subscribers do their wiring in the ctor, so StartAsync stays empty —
|
||||||
|
// the registration alone forces an eager resolve which runs that wiring.
|
||||||
|
|
||||||
|
internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly IpcManager _ipc = ipc;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TypingIpcInitHostedService(TypingIpc typingIpc) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly TypingIpc _typingIpc = typingIpc;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ExtraChatInitHostedService(ExtraChat extraChat) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ExtraChat _extraChat = extraChat;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class MessageManagerInitHostedService(
|
||||||
|
IDalamudPluginInterface pluginInterface,
|
||||||
|
MessageManager manager
|
||||||
|
) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// FilterAllTabsAsync rebuilds the per-tab view from the message store;
|
||||||
|
// on Boot, tabs come up empty and the first chat events fill them, so
|
||||||
|
// we skip the rebuild to avoid a pointless full-history scan.
|
||||||
|
if (pluginInterface.Reason is not PluginLoadReason.Boot)
|
||||||
|
manager.FilterAllTabsAsync();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService service)
|
||||||
|
: IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
service.Initialize();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
internal sealed class DalamudLogger : ILogger
|
||||||
|
{
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
|
||||||
|
public DalamudLogger(string name, IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDisposable? ILogger.BeginScope<TState>(TState state) => default!;
|
||||||
|
|
||||||
|
// Filtering happens in Dalamud's /xllog. Letting every level through keeps
|
||||||
|
// the HellionChat side stateless; if we ever want a per-plugin floor we add
|
||||||
|
// a Config.LogLevel and tighten this method.
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => true;
|
||||||
|
|
||||||
|
public void Log<TState>(
|
||||||
|
LogLevel logLevel,
|
||||||
|
EventId eventId,
|
||||||
|
TState state,
|
||||||
|
Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// U+200B between the bracket and the level is a quiet provenance
|
||||||
|
// marker; byte-distinguishable from any 1:1 port of this format.
|
||||||
|
if ((int)logLevel <= (int)LogLevel.Information)
|
||||||
|
{
|
||||||
|
_pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append($"[{_name}]{{{(int)logLevel}}} {state} {exception?.Message}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(exception?.StackTrace))
|
||||||
|
sb.AppendLine(exception.StackTrace);
|
||||||
|
|
||||||
|
var inner = exception?.InnerException;
|
||||||
|
while (inner != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"InnerException {inner}: {inner.Message}");
|
||||||
|
sb.AppendLine(inner.StackTrace);
|
||||||
|
inner = inner.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logLevel == LogLevel.Warning)
|
||||||
|
_pluginLog.Warning(sb.ToString());
|
||||||
|
else if (logLevel == LogLevel.Error)
|
||||||
|
_pluginLog.Error(sb.ToString());
|
||||||
|
else
|
||||||
|
_pluginLog.Fatal(sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
[ProviderAlias("Dalamud")]
|
||||||
|
public sealed class DalamudLoggingProvider : ILoggerProvider
|
||||||
|
{
|
||||||
|
// Hellion Forge Bronze (#C2410C). Mixed into the bootstrap fingerprint.
|
||||||
|
private const string HellionMarker = "HellionForgeBronzeC2410C";
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers = new(
|
||||||
|
StringComparer.OrdinalIgnoreCase
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
|
||||||
|
public DalamudLoggingProvider(IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
EmitBootstrapBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-shot per plugin load. Intentionally visible in xllog so uncredited
|
||||||
|
// ports of the DalamudLogger trio keep announcing their origin.
|
||||||
|
private void EmitBootstrapBanner()
|
||||||
|
{
|
||||||
|
var version =
|
||||||
|
typeof(DalamudLoggingProvider).Assembly.GetName().Version?.ToString() ?? "0.0.0";
|
||||||
|
var fingerprint = ComputeFingerprint(version);
|
||||||
|
_pluginLog.Information(
|
||||||
|
$"HellionChat DI-Logger bootstrap v{version} fingerprint={fingerprint}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeFingerprint(string version)
|
||||||
|
{
|
||||||
|
var seed = Encoding.UTF8.GetBytes($"{HellionMarker}-{version}");
|
||||||
|
var hash = SHA256.HashData(seed);
|
||||||
|
var sb = new StringBuilder(8);
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
sb.Append(hash[i].ToString("x2"));
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
{
|
||||||
|
// Category-name normalisation mirrors Lightless: take the leaf type
|
||||||
|
// name, then either ellipsis-trim long ones or left-pad short ones to
|
||||||
|
// 15 chars so the xllog column stays aligned across services.
|
||||||
|
var catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last();
|
||||||
|
if (catName.Length > 15)
|
||||||
|
catName = string.Concat(
|
||||||
|
catName.AsSpan(0, 6),
|
||||||
|
"...",
|
||||||
|
catName.AsSpan(catName.Length - 6, 6)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
catName = catName.PadLeft(15);
|
||||||
|
|
||||||
|
return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _pluginLog));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_loggers.Clear();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
public static class DalamudLoggingProviderExtensions
|
||||||
|
{
|
||||||
|
public static ILoggingBuilder AddDalamudLogging(
|
||||||
|
this ILoggingBuilder builder,
|
||||||
|
IPluginLog pluginLog
|
||||||
|
)
|
||||||
|
{
|
||||||
|
builder.ClearProviders();
|
||||||
|
builder.Services.TryAddEnumerable(
|
||||||
|
ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>(
|
||||||
|
_ => new DalamudLoggingProvider(pluginLog)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace HellionChat.Integrations;
|
namespace HellionChat.Integrations;
|
||||||
@@ -23,7 +24,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
private readonly ICallGateSubscriber<object> _ready;
|
private readonly ICallGateSubscriber<object> _ready;
|
||||||
private readonly ICallGateSubscriber<object> _disposing;
|
private readonly ICallGateSubscriber<object> _disposing;
|
||||||
|
|
||||||
private readonly IPluginLog _log;
|
private readonly ILogger<HonorificService> _logger;
|
||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private bool _versionWarningLogged;
|
private bool _versionWarningLogged;
|
||||||
|
|
||||||
@@ -34,12 +35,12 @@ internal sealed class HonorificService : IDisposable
|
|||||||
|
|
||||||
public HonorificService(
|
public HonorificService(
|
||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
IPluginLog log,
|
ILogger<HonorificService> logger,
|
||||||
IFramework framework
|
IFramework framework
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_log = log;
|
_logger = logger;
|
||||||
|
|
||||||
// Gate objects are cached per-name by Dalamud and safe to register
|
// Gate objects are cached per-name by Dalamud and safe to register
|
||||||
// before Honorific loads — they just won't fire until it does.
|
// before Honorific loads — they just won't fire until it does.
|
||||||
@@ -84,7 +85,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
{
|
{
|
||||||
if (!_versionWarningLogged)
|
if (!_versionWarningLogged)
|
||||||
{
|
{
|
||||||
_log.Warning(
|
_logger.LogWarning(
|
||||||
"Honorific API version mismatch — expected major 3, "
|
"Honorific API version mismatch — expected major 3, "
|
||||||
+ "found {Major}.{Minor}. Disabling Honorific integration.",
|
+ "found {Major}.{Minor}. Disabling Honorific integration.",
|
||||||
version.Item1,
|
version.Item1,
|
||||||
@@ -104,7 +105,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Honorific not installed or not yet initialised — Ready will retry.
|
// Honorific not installed or not yet initialised — Ready will retry.
|
||||||
_log.Debug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
|
_logger.LogDebug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
|
||||||
IsAvailable = false;
|
IsAvailable = false;
|
||||||
CurrentTitle = null;
|
CurrentTitle = null;
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
{
|
{
|
||||||
// Warning not Debug — a silent unsubscribe failure leaks a live
|
// Warning not Debug — a silent unsubscribe failure leaks a live
|
||||||
// subscription across plugin reloads.
|
// subscription across plugin reloads.
|
||||||
_log.Warning(
|
_logger.LogWarning(
|
||||||
ex,
|
ex,
|
||||||
"Honorific unsubscribe failed (likely API break or gate already gone)."
|
"Honorific unsubscribe failed (likely API break or gate already gone)."
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,10 +4,19 @@ namespace HellionChat.Integrations;
|
|||||||
|
|
||||||
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||||
// so HellionChat loads cleanly when Honorific is absent.
|
// so HellionChat loads cleanly when Honorific is absent.
|
||||||
// Glow/gradient fields omitted; Cycle 1 renders primary Color only.
|
//
|
||||||
|
// v1.4.7: render Glow only. Gradient (Color3 / GradientColourSet / Style) is
|
||||||
|
// parsed and stashed so a future cycle can render it without re-shaping the
|
||||||
|
// JSON roundtrip — see vault anchor "Honorific Full Gradient Port" (would
|
||||||
|
// need GradientSystem.cs + the hardcoded Pride-palette list ported, or an
|
||||||
|
// upstream IPC PR exposing the resolved frame colour).
|
||||||
internal sealed record HonorificTitleData(
|
internal sealed record HonorificTitleData(
|
||||||
string? Title,
|
string? Title,
|
||||||
bool IsPrefix,
|
bool IsPrefix,
|
||||||
bool IsOriginal,
|
bool IsOriginal,
|
||||||
Vector3? Color
|
Vector3? Color,
|
||||||
|
Vector3? Glow,
|
||||||
|
Vector3? Color3,
|
||||||
|
int? GradientColourSet,
|
||||||
|
string? GradientAnimationStyle
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ipc;
|
namespace HellionChat.Ipc;
|
||||||
|
|
||||||
public sealed class ExtraChat : IDisposable
|
public sealed class ExtraChat : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<ExtraChat> _logger;
|
||||||
|
|
||||||
#pragma warning disable CS0649 // Assigned through IPC
|
#pragma warning disable CS0649 // Assigned through IPC
|
||||||
[Serializable]
|
[Serializable]
|
||||||
private struct OverrideInfo
|
private struct OverrideInfo
|
||||||
@@ -36,8 +39,9 @@ public sealed class ExtraChat : IDisposable
|
|||||||
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
||||||
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
||||||
|
|
||||||
internal ExtraChat()
|
internal ExtraChat(ILogger<ExtraChat> logger)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>(
|
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>(
|
||||||
"ExtraChat.OverrideChannelColour"
|
"ExtraChat.OverrideChannelColour"
|
||||||
);
|
);
|
||||||
@@ -62,7 +66,7 @@ public sealed class ExtraChat : IDisposable
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
|
// ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
|
||||||
Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
_logger.LogTrace(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ipc;
|
namespace HellionChat.Ipc;
|
||||||
|
|
||||||
@@ -19,12 +20,26 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
||||||
private ICallGateProvider<ChatInputState, object?> StateChangedGate { 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 ChatInputState LastState;
|
||||||
private bool HasState;
|
private bool HasState;
|
||||||
|
|
||||||
internal TypingIpc(Plugin plugin)
|
private readonly ILogger<TypingIpc> _logger;
|
||||||
|
|
||||||
|
internal TypingIpc(Plugin plugin, ILogger<TypingIpc> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||||
"HellionChat.GetChatInputState"
|
"HellionChat.GetChatInputState"
|
||||||
@@ -33,7 +48,16 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
"HellionChat.ChatInputStateChanged"
|
"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);
|
StateQueryGate.RegisterFunc(GetState);
|
||||||
|
ChatTwoStateQueryGate.RegisterFunc(GetState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatInputState BuildState()
|
private ChatInputState BuildState()
|
||||||
@@ -67,10 +91,13 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
HasState = true;
|
HasState = true;
|
||||||
LastState = state;
|
LastState = state;
|
||||||
StateChangedGate.SendMessage(state);
|
StateChangedGate.SendMessage(state);
|
||||||
|
// v1.4.9 R4: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
|
||||||
|
ChatTwoStateChangedGate.SendMessage(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
StateQueryGate.UnregisterFunc();
|
StateQueryGate.UnregisterFunc();
|
||||||
|
ChatTwoStateQueryGate.UnregisterFunc();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
internal sealed class IpcManager : IDisposable
|
internal sealed class IpcManager : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<IpcManager> _logger;
|
||||||
|
|
||||||
private ICallGateProvider<string> RegisterGate { get; }
|
private ICallGateProvider<string> RegisterGate { get; }
|
||||||
private ICallGateProvider<string, object?> UnregisterGate { get; }
|
private ICallGateProvider<string, object?> UnregisterGate { get; }
|
||||||
private ICallGateProvider<object?> AvailableGate { get; }
|
private ICallGateProvider<object?> AvailableGate { get; }
|
||||||
@@ -19,10 +22,31 @@ internal sealed class IpcManager : IDisposable
|
|||||||
object?
|
object?
|
||||||
> InvokeGate { get; }
|
> 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; } = [];
|
internal List<string> Registered { get; } = [];
|
||||||
|
|
||||||
public IpcManager()
|
public IpcManager(ILogger<IpcManager> logger)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
|
RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
|
||||||
RegisterGate.RegisterFunc(Register);
|
RegisterGate.RegisterFunc(Register);
|
||||||
|
|
||||||
@@ -41,7 +65,32 @@ internal sealed class IpcManager : IDisposable
|
|||||||
object?
|
object?
|
||||||
>("HellionChat.Invoke");
|
>("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();
|
AvailableGate.SendMessage();
|
||||||
|
ChatTwoAvailableGate.SendMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Invoke(
|
internal void Invoke(
|
||||||
@@ -54,6 +103,8 @@ internal sealed class IpcManager : IDisposable
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
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()
|
private string Register()
|
||||||
@@ -72,6 +123,8 @@ internal sealed class IpcManager : IDisposable
|
|||||||
{
|
{
|
||||||
UnregisterGate.UnregisterAction();
|
UnregisterGate.UnregisterAction();
|
||||||
RegisterGate.UnregisterFunc();
|
RegisterGate.UnregisterFunc();
|
||||||
|
ChatTwoUnregisterGate.UnregisterAction();
|
||||||
|
ChatTwoRegisterGate.UnregisterFunc();
|
||||||
Registered.Clear();
|
Registered.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ public partial class Message
|
|||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Failed to parse extra chat channel GUID");
|
Plugin.LogProxy.Error(ex, "Failed to parse extra chat channel GUID");
|
||||||
Plugin.Log.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
Plugin.LogProxy.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
||||||
return Guid.Empty;
|
return Guid.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,7 @@ public partial class Message
|
|||||||
AddChunkWithMessage(
|
AddChunkWithMessage(
|
||||||
text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
|
text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
|
||||||
);
|
);
|
||||||
Plugin.Log.Debug(
|
Plugin.LogProxy.Debug(
|
||||||
$"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
|
$"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ public partial class Message
|
|||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
||||||
Plugin.Log.Debug($"Failed to parse the text param: '{split}'");
|
Plugin.LogProxy.Debug($"Failed to parse the text param: '{split}'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using HellionChat.Util;
|
|||||||
using Lumina.Text.Expressions;
|
using Lumina.Text.Expressions;
|
||||||
using Lumina.Text.Payloads;
|
using Lumina.Text.Payloads;
|
||||||
using Lumina.Text.ReadOnly;
|
using Lumina.Text.ReadOnly;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
internal const int MessageDisplayLimit = 10_000;
|
internal const int MessageDisplayLimit = 10_000;
|
||||||
|
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
|
private readonly ILogger<MessageManager> _logger;
|
||||||
internal MessageStore Store { get; }
|
internal MessageStore Store { get; }
|
||||||
|
|
||||||
private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
|
private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
|
||||||
@@ -48,11 +50,21 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// AutoTellTabsService to spawn or refresh temp tabs without coupling.
|
// AutoTellTabsService to spawn or refresh temp tabs without coupling.
|
||||||
public event Action<Message>? MessageProcessed;
|
public event Action<Message>? MessageProcessed;
|
||||||
|
|
||||||
internal unsafe MessageManager(Plugin plugin)
|
internal unsafe MessageManager(
|
||||||
|
Plugin plugin,
|
||||||
|
ILogger<MessageManager> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil);
|
Store = new MessageStore(
|
||||||
|
DatabasePath(),
|
||||||
|
Plugin.PlatformUtil,
|
||||||
|
loggerFactory.CreateLogger<MessageStore>(),
|
||||||
|
loggerFactory
|
||||||
|
);
|
||||||
|
|
||||||
PendingMessageThread = new Thread(() =>
|
PendingMessageThread = new Thread(() =>
|
||||||
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||||
@@ -91,7 +103,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
|
|
||||||
if (PendingMessageThread.IsAlive)
|
if (PendingMessageThread.IsAlive)
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
"PendingMessageThread did not observe cancellation within 10s. "
|
"PendingMessageThread did not observe cancellation within 10s. "
|
||||||
+ "Worker remains on background thread; next plugin reload releases it."
|
+ "Worker remains on background thread; next plugin reload releases it."
|
||||||
);
|
);
|
||||||
@@ -137,7 +149,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error processing pending message");
|
_logger.LogError(ex, "Error processing pending message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -182,10 +194,12 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
|
|
||||||
// Mark failed messages as deleted to prevent retry attempts
|
// Mark failed messages as deleted to prevent retry attempts
|
||||||
var failedIds = messages.FailedMessageIds();
|
var failedIds = messages.FailedMessageIds();
|
||||||
Plugin.Log.Info($"Marking {failedIds.Count} messages as deleted due to parse failures");
|
_logger.LogInformation(
|
||||||
|
$"Marking {failedIds.Count} messages as deleted due to parse failures"
|
||||||
|
);
|
||||||
foreach (var msgId in messages.FailedMessageIds())
|
foreach (var msgId in messages.FailedMessageIds())
|
||||||
{
|
{
|
||||||
Plugin.Log.Debug($"Marking message '{msgId}' as deleted due to parse failure");
|
_logger.LogDebug($"Marking message '{msgId}' as deleted due to parse failure");
|
||||||
Store.DeleteMessage(msgId);
|
Store.DeleteMessage(msgId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,10 +215,13 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in FilterAllTabs");
|
_logger.LogError(ex, "Error in FilterAllTabs");
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Log.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.
|
||||||
|
_logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +276,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in ContentIdResolver");
|
_logger.LogError(ex, "Error in ContentIdResolver");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+677
-253
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Ui;
|
using HellionChat.Ui;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Action = System.Action;
|
using Action = System.Action;
|
||||||
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
||||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||||
@@ -40,9 +41,12 @@ public sealed class PayloadHandler
|
|||||||
|
|
||||||
private const uint PopupSfx = 1;
|
private const uint PopupSfx = 1;
|
||||||
|
|
||||||
internal PayloadHandler(ChatLogWindow logWindow)
|
private readonly ILogger<PayloadHandler> _logger;
|
||||||
|
|
||||||
|
internal PayloadHandler(ChatLogWindow logWindow, ILogger<PayloadHandler> logger)
|
||||||
{
|
{
|
||||||
LogWindow = logWindow;
|
LogWindow = logWindow;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Draw()
|
internal void Draw()
|
||||||
@@ -131,7 +135,7 @@ public sealed class PayloadHandler
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error executing integration");
|
_logger.LogError(ex, "Error executing integration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +539,7 @@ public sealed class PayloadHandler
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Could not find DalamudLinkHandlers");
|
_logger.LogWarning("Could not find DalamudLinkHandlers");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +550,7 @@ public sealed class PayloadHandler
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error executing DalamudLinkPayload handler");
|
_logger.LogError(ex, "Error executing DalamudLinkPayload handler");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+391
-119
@@ -14,6 +14,9 @@ using HellionChat.Ipc;
|
|||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Ui;
|
using HellionChat.Ui;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -117,11 +120,42 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
||||||
internal static IPlatformUtil PlatformUtil { get; private set; } = null!;
|
internal static IPlatformUtil PlatformUtil { get; private set; } = null!;
|
||||||
|
|
||||||
|
// Log indirection over Dalamud's IPluginLog. Same rationale as PlatformUtil:
|
||||||
|
// call-sites read through LogProxy so MessageStore can be tested in
|
||||||
|
// isolation. Wired immediately after Dalamud injects Log.
|
||||||
|
internal static IPluginLogProxy LogProxy { get; private set; } = null!;
|
||||||
|
|
||||||
|
// Nullable so DisposeAsync stays safe if Host-build throws before the
|
||||||
|
// fields get assigned — Dalamud fires DisposeAsync regardless.
|
||||||
|
private readonly IHost? _host;
|
||||||
|
private readonly PluginLifecycle? _lifecycle;
|
||||||
|
|
||||||
|
// 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.
|
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||||
private int _disposeStarted;
|
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;
|
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
|
// 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
|
// can't run in parallel. Volatile because the ImGui thread reads it outside
|
||||||
// the lock to gate the manual button.
|
// the lock to gate the manual button.
|
||||||
@@ -158,30 +192,99 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||||
|
|
||||||
// Wire platform indirection before LoadAsync allocates anything that
|
// PlatformUtil and LogProxy are filled from the DI container in
|
||||||
// needs Util.* — services then read Plugin.PlatformUtil instead of
|
// Phase-1 below (`_host.Services.GetRequiredService<IPlatformUtil>()`
|
||||||
// hitting the Dalamud static surface directly.
|
// and the LogProxy equivalent). Phase-0 helpers that run before that
|
||||||
PlatformUtil = new DalamudPlatformUtil();
|
// point (MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize)
|
||||||
|
// do not touch either static, so the brief null-window is safe.
|
||||||
|
|
||||||
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
// Schema gate: v1.4.x requires config v16+. Users on older schemas
|
||||||
// must install v1.4.2 first to run the migration chain.
|
// must install v1.4.2 first to run the migration chain. v17 adds
|
||||||
|
// Tab.IsPinned (additive, no data migration needed) so v16 configs
|
||||||
|
// load cleanly and get their Version stamp bumped after the gate.
|
||||||
if (Config.Version < 16)
|
if (Config.Version < 16)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"HellionChat v1.4.6 requires config schema v16, got v{Config.Version}. "
|
$"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.6."
|
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Config.Version = 17;
|
||||||
|
|
||||||
// Session-only tabs are stripped on every load; AutoTellTabsService.Initialize
|
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||||
// then re-pegs TempTabCounter from the stripped list, not the pre-strip snapshot.
|
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
||||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
|
||||||
|
|
||||||
LanguageChanged(Interface.UiLanguage);
|
LanguageChanged(Interface.UiLanguage);
|
||||||
ImGuiUtil.Initialize(this);
|
ImGuiUtil.Initialize(this);
|
||||||
|
|
||||||
DeferredSaveFrames = -1;
|
DeferredSaveFrames = -1;
|
||||||
|
|
||||||
|
// Custom themes dir + seed run before the container builds so the
|
||||||
|
// ThemeRegistry factory lambda finds the directory ready.
|
||||||
|
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(customThemesDir);
|
||||||
|
SeedExampleThemeIfEmpty(customThemesDir);
|
||||||
|
|
||||||
|
// Phase-1: build the host synchronously (the schema gate must clear
|
||||||
|
// before services allocate; Lightless' deferred build would invert
|
||||||
|
// that order) and pull singletons into the Plugin.X surface.
|
||||||
|
var dependencies = new PluginHostDependencies(
|
||||||
|
Interface,
|
||||||
|
Log,
|
||||||
|
ChatGui,
|
||||||
|
ClientState,
|
||||||
|
CommandManager,
|
||||||
|
Condition,
|
||||||
|
DataManager,
|
||||||
|
Framework,
|
||||||
|
GameGui,
|
||||||
|
KeyState,
|
||||||
|
ObjectTable,
|
||||||
|
PartyList,
|
||||||
|
TargetManager,
|
||||||
|
TextureProvider,
|
||||||
|
GameInteropProvider,
|
||||||
|
GameConfig,
|
||||||
|
Notification,
|
||||||
|
AddonLifecycle,
|
||||||
|
PlayerState,
|
||||||
|
Evaluator,
|
||||||
|
SelfTestRegistry
|
||||||
|
);
|
||||||
|
|
||||||
|
_host = PluginHostFactory.Build(this, dependencies);
|
||||||
|
_lifecycle = _host.Services.GetRequiredService<PluginLifecycle>();
|
||||||
|
_lifecycle.Host = _host;
|
||||||
|
|
||||||
|
// Plugin.X static bridge - filled from the container so DI-aware code
|
||||||
|
// and the ~93 Plugin.X consumer sites read the same instances.
|
||||||
|
PlatformUtil = _host.Services.GetRequiredService<IPlatformUtil>();
|
||||||
|
LogProxy = _host.Services.GetRequiredService<IPluginLogProxy>();
|
||||||
|
FileDialogManager = _host.Services.GetRequiredService<FileDialogManager>();
|
||||||
|
|
||||||
|
// Resolve order matters: block-B services first so the windows can
|
||||||
|
// read Plugin.MessageManager etc. from their own ctors without NREs.
|
||||||
|
FontManager = _host.Services.GetRequiredService<FontManager>();
|
||||||
|
ThemeRegistry = _host.Services.GetRequiredService<Themes.ThemeRegistry>();
|
||||||
|
Commands = _host.Services.GetRequiredService<Commands>();
|
||||||
|
Functions = _host.Services.GetRequiredService<GameFunctions.GameFunctions>();
|
||||||
|
Ipc = _host.Services.GetRequiredService<IpcManager>();
|
||||||
|
TypingIpc = _host.Services.GetRequiredService<TypingIpc>();
|
||||||
|
ExtraChat = _host.Services.GetRequiredService<ExtraChat>();
|
||||||
|
HonorificService = _host.Services.GetRequiredService<Integrations.HonorificService>();
|
||||||
|
StatusBar = _host.Services.GetRequiredService<Ui.StatusBar>();
|
||||||
|
MessageManager = _host.Services.GetRequiredService<MessageManager>();
|
||||||
|
AutoTellTabsService = _host.Services.GetRequiredService<AutoTellTabsService>();
|
||||||
|
|
||||||
|
ChatLogWindow = _host.Services.GetRequiredService<ChatLogWindow>();
|
||||||
|
SettingsWindow = _host.Services.GetRequiredService<SettingsWindow>();
|
||||||
|
DbViewer = _host.Services.GetRequiredService<DbViewer>();
|
||||||
|
InputPreview = _host.Services.GetRequiredService<InputPreview>();
|
||||||
|
CommandHelpWindow = _host.Services.GetRequiredService<CommandHelpWindow>();
|
||||||
|
SeStringDebugger = _host.Services.GetRequiredService<SeStringDebugger>();
|
||||||
|
DebuggerWindow = _host.Services.GetRequiredService<DebuggerWindow>();
|
||||||
|
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAsync(CancellationToken cancellationToken)
|
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||||
@@ -203,65 +306,28 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// BuildFonts registers handles with Dalamud's FontAtlas; the atlas
|
// Container drives service init now: Host.StartAsync triggers the
|
||||||
// rebuilds async a few frames later (visible "font-pop" on first load).
|
// IHostedService adapters (FontManager.BuildFonts, ThemeRegistry
|
||||||
FontManager = new FontManager();
|
// cache warmup + Switch, IPC eager-resolve, MessageManager
|
||||||
FontManager.BuildFonts();
|
// FilterAllTabsAsync, AutoTellTabsService.Initialize). Window
|
||||||
|
// registration with WindowSystem runs on the framework thread
|
||||||
// ThemeRegistry must be wired before the first Draw tick.
|
// inside PluginLifecycle.LoadAsync after StartAsync returns.
|
||||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
if (_lifecycle is not null)
|
||||||
Directory.CreateDirectory(customThemesDir);
|
await _lifecycle.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
SeedExampleThemeIfEmpty(customThemesDir);
|
|
||||||
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
|
||||||
ThemeRegistry.Switch(Config.Theme);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Service allocations — order encodes dependencies.
|
|
||||||
// HonorificService registers IPC subscribers early to catch
|
|
||||||
// Ready/Disposing events from the first frame.
|
|
||||||
FileDialogManager = new FileDialogManager();
|
|
||||||
Commands = new Commands();
|
|
||||||
Functions = new GameFunctions.GameFunctions(this);
|
|
||||||
Ipc = new IpcManager();
|
|
||||||
TypingIpc = new TypingIpc(this);
|
|
||||||
ExtraChat = new ExtraChat();
|
|
||||||
HonorificService = new Integrations.HonorificService(Interface, Log, Framework);
|
|
||||||
StatusBar = new Ui.StatusBar();
|
|
||||||
MessageManager = new MessageManager(this);
|
|
||||||
|
|
||||||
AutoTellTabsService = new AutoTellTabsService(
|
|
||||||
this,
|
|
||||||
MessageManager,
|
|
||||||
MessageManager.Store
|
|
||||||
);
|
|
||||||
AutoTellTabsService.Initialize();
|
|
||||||
|
|
||||||
SelfTestRegistry.RegisterTestSteps([new SelfTests.ThemeSwitchSelfTestStep(this)]);
|
SelfTestRegistry.RegisterTestSteps([new SelfTests.ThemeSwitchSelfTestStep(this)]);
|
||||||
|
|
||||||
ChatLogWindow = new ChatLogWindow(this);
|
|
||||||
SettingsWindow = new SettingsWindow(this);
|
|
||||||
DbViewer = new DbViewer(this);
|
|
||||||
InputPreview = new InputPreview(ChatLogWindow);
|
|
||||||
CommandHelpWindow = new CommandHelpWindow(ChatLogWindow);
|
|
||||||
SeStringDebugger = new SeStringDebugger(this);
|
|
||||||
DebuggerWindow = new DebuggerWindow(this);
|
|
||||||
FirstRunWizard = new FirstRunWizard(this);
|
|
||||||
|
|
||||||
WindowSystem.AddWindow(ChatLogWindow);
|
|
||||||
WindowSystem.AddWindow(SettingsWindow);
|
|
||||||
WindowSystem.AddWindow(DbViewer);
|
|
||||||
WindowSystem.AddWindow(InputPreview);
|
|
||||||
WindowSystem.AddWindow(CommandHelpWindow);
|
|
||||||
WindowSystem.AddWindow(SeStringDebugger);
|
|
||||||
WindowSystem.AddWindow(DebuggerWindow);
|
|
||||||
WindowSystem.AddWindow(FirstRunWizard);
|
|
||||||
|
|
||||||
if (!Config.FirstRunCompleted)
|
if (!Config.FirstRunCompleted)
|
||||||
FirstRunWizard.IsOpen = true;
|
FirstRunWizard.IsOpen = true;
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
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();
|
Commands.Initialise();
|
||||||
|
|
||||||
// Daily retention sweep — fire-and-forget, skips when disabled
|
// Daily retention sweep — fire-and-forget, skips when disabled
|
||||||
@@ -271,8 +337,115 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
if (Config.ShowEmotes)
|
if (Config.ShowEmotes)
|
||||||
_ = EmoteCache.LoadData();
|
_ = EmoteCache.LoadData();
|
||||||
|
|
||||||
if (Interface.Reason is not PluginLoadReason.Boot)
|
// FilterAllTabsAsync now runs from MessageManagerInitHostedService
|
||||||
MessageManager.FilterAllTabsAsync();
|
// during Host.StartAsync (same Reason-not-Boot guard there).
|
||||||
|
|
||||||
|
// 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.DisableCutsceneUiHide = true;
|
||||||
Interface.UiBuilder.DisableGposeUiHide = true;
|
Interface.UiBuilder.DisableGposeUiHide = true;
|
||||||
@@ -290,7 +463,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
Framework.Update += FrameworkUpdate;
|
Framework.Update += FrameworkUpdate;
|
||||||
Interface.UiBuilder.Draw += Draw;
|
Interface.UiBuilder.Draw += Draw;
|
||||||
Interface.LanguageChanged += LanguageChanged;
|
Interface.LanguageChanged += LanguageChanged;
|
||||||
Interface.UiBuilder.OpenMainUi += OpenMainUi;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -312,14 +484,32 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
||||||
return;
|
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;
|
Exception? failure = null;
|
||||||
|
|
||||||
// Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync.
|
// 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.LanguageChanged -= LanguageChanged);
|
||||||
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
|
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
|
||||||
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
|
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.
|
// Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore.
|
||||||
failure = CaptureFailure(
|
failure = CaptureFailure(
|
||||||
failure,
|
failure,
|
||||||
@@ -333,44 +523,18 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unsubscribe AutoTellTabs before MessageManager goes away.
|
// Framework-thread cleanup the container does not reach.
|
||||||
failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose());
|
|
||||||
|
|
||||||
// MessageManager has its own async dispose path (DB flush, thread shutdown).
|
|
||||||
if (MessageManager is not null)
|
|
||||||
{
|
|
||||||
failure = await CaptureFailureAsync(
|
|
||||||
failure,
|
|
||||||
() => MessageManager.DisposeAsync().AsTask()
|
|
||||||
)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game-function / IPC / window cleanup must run on the framework thread.
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Framework
|
await Framework
|
||||||
.RunOnFrameworkThread(() =>
|
.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
|
failure = CaptureFailure(failure, TearDownCommands);
|
||||||
failure = CaptureFailure(
|
failure = CaptureFailure(
|
||||||
failure,
|
failure,
|
||||||
() => GameFunctions.GameFunctions.SetChatInteractable(true)
|
() => GameFunctions.GameFunctions.SetChatInteractable(true)
|
||||||
);
|
);
|
||||||
|
|
||||||
// IPC subscribers before windows — prevents a final IPC event
|
|
||||||
// from reaching a half-torn ChatLogWindow.
|
|
||||||
failure = CaptureFailure(failure, () => HonorificService?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => TypingIpc?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => ExtraChat?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => Ipc?.Dispose());
|
|
||||||
|
|
||||||
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
|
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
|
||||||
failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => DbViewer?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => InputPreview?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => SettingsWindow?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => DebuggerWindow?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => SeStringDebugger?.Dispose());
|
|
||||||
})
|
})
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -379,11 +543,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
failure ??= ex;
|
failure ??= ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pure-memory cleanups — no Framework / UI / IPC touch.
|
// Container disposes services + windows on the framework thread.
|
||||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
// MessageManager.DisposeAsync is not idempotent, so we let the
|
||||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
// container do it once instead of double-disposing.
|
||||||
|
if (_lifecycle is not null)
|
||||||
|
{
|
||||||
|
failure = await CaptureFailureAsync(failure, () => _lifecycle.DisposeAsync().AsTask())
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static-class cleanups the container has no handle on.
|
||||||
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
||||||
// Static input history would otherwise survive the plugin reload.
|
|
||||||
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
||||||
|
|
||||||
if (failure is not null)
|
if (failure is not null)
|
||||||
@@ -530,11 +700,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()
|
private void RunRetentionSweepIfDue()
|
||||||
{
|
{
|
||||||
if (!Config.RetentionEnabled)
|
if (!Config.RetentionEnabled)
|
||||||
@@ -570,15 +824,31 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
if (deleted > 0)
|
if (deleted > 0)
|
||||||
{
|
{
|
||||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||||
// Run clear+refilter on the framework thread — FilterAllTabsAsync
|
// Schedule on the next framework tick to avoid the ~194ms
|
||||||
// is fire-and-forget and would race the next sweep cycle.
|
// hitch from blocking with .Wait() while the framework
|
||||||
Framework
|
// finishes the current frame. Tabs-list mutation must
|
||||||
.Run(() =>
|
// 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.ClearAllTabs();
|
||||||
MessageManager.FilterAllTabs();
|
MessageManager.FilterAllTabs();
|
||||||
})
|
}
|
||||||
.Wait();
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Retention sweep clear+refilter failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -602,6 +872,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
private void Draw()
|
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.
|
// Theme engine is always active; Classic is a theme, not a disabled state.
|
||||||
using IDisposable _style = HellionStyle.PushGlobal(
|
using IDisposable _style = HellionStyle.PushGlobal(
|
||||||
ThemeRegistry.Active,
|
ThemeRegistry.Active,
|
||||||
@@ -646,21 +922,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
internal void SaveConfig()
|
internal void SaveConfig()
|
||||||
{
|
{
|
||||||
// Session-only Auto-Tell-Tabs aren't persisted, so they move aside
|
// Only unpinned TempTabs are session-only — they move aside before
|
||||||
// before serialization and re-attach after. Cloning only the temp
|
// serialization and re-attach after. Pinned TempTabs stay in
|
||||||
// subset keeps the allocation proportional to AutoTellTabsLimit
|
// Config.Tabs across the save so JSON includes them. Cloning only the
|
||||||
// (<=15) instead of the full tab list.
|
// unpinned subset keeps the allocation proportional to
|
||||||
var tempTabs = Config.Tabs.Where(t => t.IsTempTab).ToList();
|
// AutoTellTabsLimit (<=15) instead of the full tab list.
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||||
|
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||||
|
|
||||||
Interface.SavePluginConfig(Config);
|
Interface.SavePluginConfig(Config);
|
||||||
|
|
||||||
Config.Tabs.AddRange(tempTabs);
|
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||||
|
|
||||||
// F2.1: the mid-step RemoveAll bypasses AutoTellTabsService, so
|
|
||||||
// re-peg the counter. Null-conditional because SaveConfig can fire
|
|
||||||
// before Phase-2 init.
|
|
||||||
AutoTellTabsService?.ResyncTempTabCounter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void LanguageChanged(string langCode)
|
internal void LanguageChanged(string langCode)
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using HellionChat.Infrastructure.Hosting;
|
||||||
|
using HellionChat.Infrastructure.Logging;
|
||||||
|
using HellionChat.Ipc;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Ui;
|
||||||
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat;
|
||||||
|
|
||||||
|
// Builds the generic-host DI container that drives v1.5.0+. The factory is
|
||||||
|
// invoked synchronously from Plugin.ctor (after the schema gate clears) so the
|
||||||
|
// container exists before PluginLifecycle.LoadAsync runs. See plan §1 for the
|
||||||
|
// deliberate divergence from Lightless' deferred Func-delegate pattern.
|
||||||
|
internal static class PluginHostFactory
|
||||||
|
{
|
||||||
|
public static IHost Build(Plugin plugin, PluginHostDependencies dependencies)
|
||||||
|
{
|
||||||
|
return new HostBuilder()
|
||||||
|
.UseContentRoot(dependencies.PluginInterface.ConfigDirectory.FullName)
|
||||||
|
.ConfigureLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.ClearProviders();
|
||||||
|
logging.AddDalamudLogging(dependencies.PluginLog);
|
||||||
|
logging.SetMinimumLevel(LogLevel.Trace);
|
||||||
|
})
|
||||||
|
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureServices(
|
||||||
|
IServiceCollection services,
|
||||||
|
Plugin plugin,
|
||||||
|
PluginHostDependencies dependencies
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Block A — Dalamud services (21 [PluginService] singletons).
|
||||||
|
services.AddSingleton(dependencies);
|
||||||
|
services.AddSingleton(dependencies.PluginInterface);
|
||||||
|
services.AddSingleton(dependencies.PluginLog);
|
||||||
|
services.AddSingleton(dependencies.ChatGui);
|
||||||
|
services.AddSingleton(dependencies.ClientState);
|
||||||
|
services.AddSingleton(dependencies.CommandManager);
|
||||||
|
services.AddSingleton(dependencies.Condition);
|
||||||
|
services.AddSingleton(dependencies.DataManager);
|
||||||
|
services.AddSingleton(dependencies.Framework);
|
||||||
|
services.AddSingleton(dependencies.GameGui);
|
||||||
|
services.AddSingleton(dependencies.KeyState);
|
||||||
|
services.AddSingleton(dependencies.ObjectTable);
|
||||||
|
services.AddSingleton(dependencies.PartyList);
|
||||||
|
services.AddSingleton(dependencies.TargetManager);
|
||||||
|
services.AddSingleton(dependencies.TextureProvider);
|
||||||
|
services.AddSingleton(dependencies.GameInteropProvider);
|
||||||
|
services.AddSingleton(dependencies.GameConfig);
|
||||||
|
services.AddSingleton(dependencies.Notification);
|
||||||
|
services.AddSingleton(dependencies.AddonLifecycle);
|
||||||
|
services.AddSingleton(dependencies.PlayerState);
|
||||||
|
services.AddSingleton(dependencies.Evaluator);
|
||||||
|
services.AddSingleton(dependencies.SelfTestRegistry);
|
||||||
|
|
||||||
|
// Self-references: Plugin and its WindowSystem already exist.
|
||||||
|
services.AddSingleton(plugin);
|
||||||
|
services.AddSingleton(plugin.WindowSystem);
|
||||||
|
services.AddSingleton<PluginLifecycle>();
|
||||||
|
|
||||||
|
// Block B — HellionChat singletons. Factory lambdas because most
|
||||||
|
// classes are internal-sealed and the default activator only sees
|
||||||
|
// public ctors.
|
||||||
|
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
|
||||||
|
services.AddSingleton<IPluginLogProxy>(sp => new DalamudPluginLogProxy(
|
||||||
|
sp.GetRequiredService<IPluginLog>()
|
||||||
|
));
|
||||||
|
services.AddSingleton<FileDialogManager>(_ => new FileDialogManager());
|
||||||
|
services.AddSingleton(sp => new Commands(sp.GetRequiredService<ILogger<Commands>>()));
|
||||||
|
services.AddSingleton(_ => new FontManager());
|
||||||
|
services.AddSingleton(_ => new StatusBar());
|
||||||
|
services.AddSingleton(sp => new IpcManager(sp.GetRequiredService<ILogger<IpcManager>>()));
|
||||||
|
services.AddSingleton(sp => new ExtraChat(sp.GetRequiredService<ILogger<ExtraChat>>()));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new ThemeRegistry(
|
||||||
|
Path.Combine(
|
||||||
|
sp.GetRequiredService<IDalamudPluginInterface>().ConfigDirectory.FullName,
|
||||||
|
"themes"
|
||||||
|
),
|
||||||
|
sp.GetRequiredService<ILogger<ThemeRegistry>>()
|
||||||
|
));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new GameFunctions.GameFunctions(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<GameFunctions.GameFunctions>>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new TypingIpc(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<TypingIpc>>()
|
||||||
|
));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new Integrations.HonorificService(
|
||||||
|
sp.GetRequiredService<IDalamudPluginInterface>(),
|
||||||
|
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
|
||||||
|
sp.GetRequiredService<IFramework>()
|
||||||
|
));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new MessageManager(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<MessageManager>>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
|
||||||
|
// MessageStore is allocated inside MessageManager.ctor; a separate
|
||||||
|
// container singleton would double-construct the SQLite handle.
|
||||||
|
services.AddSingleton(sp =>
|
||||||
|
{
|
||||||
|
var pluginRef = sp.GetRequiredService<Plugin>();
|
||||||
|
var manager = sp.GetRequiredService<MessageManager>();
|
||||||
|
return new AutoTellTabsService(
|
||||||
|
pluginRef,
|
||||||
|
manager,
|
||||||
|
manager.Store,
|
||||||
|
sp.GetRequiredService<ILogger<AutoTellTabsService>>()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block C — Windows. WindowSystem.AddWindow is called from
|
||||||
|
// PluginLifecycle.LoadAsync on the framework thread.
|
||||||
|
services.AddSingleton(sp => new ChatLogWindow(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<ChatLogWindow>>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new SettingsWindow(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new DbViewer(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<DbViewer>>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new InputPreview(sp.GetRequiredService<ChatLogWindow>()));
|
||||||
|
services.AddSingleton(sp => new CommandHelpWindow(sp.GetRequiredService<ChatLogWindow>()));
|
||||||
|
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
|
||||||
|
services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>()));
|
||||||
|
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
|
||||||
|
|
||||||
|
// Hosted-service adapters: thin wrappers around the existing init
|
||||||
|
// methods so the service class bodies stay unchanged.
|
||||||
|
services.AddHostedService(sp => new FontManagerInitHostedService(
|
||||||
|
sp.GetRequiredService<FontManager>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new ThemeRegistryInitHostedService(
|
||||||
|
sp.GetRequiredService<ThemeRegistry>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new IpcManagerInitHostedService(
|
||||||
|
sp.GetRequiredService<IpcManager>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new TypingIpcInitHostedService(
|
||||||
|
sp.GetRequiredService<TypingIpc>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new ExtraChatInitHostedService(
|
||||||
|
sp.GetRequiredService<ExtraChat>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new MessageManagerInitHostedService(
|
||||||
|
sp.GetRequiredService<IDalamudPluginInterface>(),
|
||||||
|
sp.GetRequiredService<MessageManager>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||||
|
sp.GetRequiredService<AutoTellTabsService>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record PluginHostDependencies(
|
||||||
|
IDalamudPluginInterface PluginInterface,
|
||||||
|
IPluginLog PluginLog,
|
||||||
|
IChatGui ChatGui,
|
||||||
|
IClientState ClientState,
|
||||||
|
ICommandManager CommandManager,
|
||||||
|
ICondition Condition,
|
||||||
|
IDataManager DataManager,
|
||||||
|
IFramework Framework,
|
||||||
|
IGameGui GameGui,
|
||||||
|
IKeyState KeyState,
|
||||||
|
IObjectTable ObjectTable,
|
||||||
|
IPartyList PartyList,
|
||||||
|
ITargetManager TargetManager,
|
||||||
|
ITextureProvider TextureProvider,
|
||||||
|
IGameInteropProvider GameInteropProvider,
|
||||||
|
IGameConfig GameConfig,
|
||||||
|
INotificationManager Notification,
|
||||||
|
IAddonLifecycle AddonLifecycle,
|
||||||
|
IPlayerState PlayerState,
|
||||||
|
ISeStringEvaluator Evaluator,
|
||||||
|
ISelfTestRegistry SelfTestRegistry
|
||||||
|
);
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace HellionChat;
|
||||||
|
|
||||||
|
// Orchestrates Host.StartAsync / StopAsync and the framework-thread dispose.
|
||||||
|
// Plugin.ctor builds the host and assigns it via the Host property, so
|
||||||
|
// PluginLifecycle never constructs the host itself.
|
||||||
|
internal sealed class PluginLifecycle : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
private readonly Plugin _plugin;
|
||||||
|
|
||||||
|
private int _disposeStarted;
|
||||||
|
private bool _hostStartRequested;
|
||||||
|
|
||||||
|
public PluginLifecycle(IFramework framework, Plugin plugin)
|
||||||
|
{
|
||||||
|
_framework = framework;
|
||||||
|
_plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin.ctor fills this immediately after PluginHostFactory.Build and
|
||||||
|
// before invoking LoadAsync; LoadAsync may NRE-suppress on Host! safely.
|
||||||
|
public IHost? Host { get; set; }
|
||||||
|
|
||||||
|
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_hostStartRequested = true;
|
||||||
|
await Host!.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// WindowSystem.AddWindow mutates an internal List<>; v1.4.9 Stage-2
|
||||||
|
// verified the list is non-thread-safe, so we marshal the entire
|
||||||
|
// registration block to the framework thread.
|
||||||
|
await _framework
|
||||||
|
.RunOnFrameworkThread(() => RegisterWindows(_plugin))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Swallow secondary dispose failure so the original load throw wins.
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterWindows(Plugin plugin)
|
||||||
|
{
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.ChatLogWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.SettingsWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.DbViewer);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.InputPreview);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.CommandHelpWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.SeStringDebugger);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||||
|
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Exception? failure = null;
|
||||||
|
|
||||||
|
if (_hostStartRequested && Host is not null)
|
||||||
|
failure = await CaptureFailureAsync(failure, () => Host.StopAsync())
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
failure = await DisposeHostOnFrameworkThreadAsync(failure).ConfigureAwait(false);
|
||||||
|
|
||||||
|
ThrowIfFailed(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Exception?> DisposeHostOnFrameworkThreadAsync(Exception? failure)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _framework
|
||||||
|
.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
failure = CaptureFailure(failure, () => Host?.Dispose());
|
||||||
|
})
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure ??= ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception? CaptureFailure(Exception? failure, Action action)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure ??= ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask<Exception?> CaptureFailureAsync(
|
||||||
|
Exception? failure,
|
||||||
|
Func<Task> action
|
||||||
|
)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await action().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure ??= ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ThrowIfFailed(Exception? failure)
|
||||||
|
{
|
||||||
|
if (failure is not null)
|
||||||
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -170,6 +170,16 @@ internal class HellionStrings
|
|||||||
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
||||||
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
||||||
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
||||||
|
internal static string PinTab_MenuPin => Get(nameof(PinTab_MenuPin));
|
||||||
|
internal static string PinTab_MenuUnpin => Get(nameof(PinTab_MenuUnpin));
|
||||||
|
internal static string PinTab_MenuPromote => Get(nameof(PinTab_MenuPromote));
|
||||||
|
internal static string PinTab_PromoteTooltip => Get(nameof(PinTab_PromoteTooltip));
|
||||||
|
internal static string PinTab_LimitReached => Get(nameof(PinTab_LimitReached));
|
||||||
|
internal static string PinTab_PinnedTooltip => Get(nameof(PinTab_PinnedTooltip));
|
||||||
|
internal static string PinTab_PinTooltip => Get(nameof(PinTab_PinTooltip));
|
||||||
|
internal static string PinTab_SectionHeader => Get(nameof(PinTab_SectionHeader));
|
||||||
|
internal static string Settings_ThemeAndLayout_SidebarWidth_Name => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Name));
|
||||||
|
internal static string Settings_ThemeAndLayout_SidebarWidth_Description => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Description));
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
|
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
|
||||||
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
||||||
@@ -260,6 +270,10 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Chat_Preview_Heading => Get(nameof(Settings_Chat_Preview_Heading));
|
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));
|
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
|
// Hellion Chat — Database-Tab section headings
|
||||||
internal static string Settings_Database_Storage_Heading => Get(nameof(Settings_Database_Storage_Heading));
|
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));
|
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
|
||||||
@@ -370,6 +384,8 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Integrations_Honorific_Status_Incompatible => Get(nameof(Settings_Integrations_Honorific_Status_Incompatible));
|
internal static string Settings_Integrations_Honorific_Status_Incompatible => Get(nameof(Settings_Integrations_Honorific_Status_Incompatible));
|
||||||
internal static string Settings_Integrations_Honorific_Toggle => Get(nameof(Settings_Integrations_Honorific_Toggle));
|
internal static string Settings_Integrations_Honorific_Toggle => Get(nameof(Settings_Integrations_Honorific_Toggle));
|
||||||
internal static string Settings_Integrations_Honorific_ToggleHint => Get(nameof(Settings_Integrations_Honorific_ToggleHint));
|
internal static string Settings_Integrations_Honorific_ToggleHint => Get(nameof(Settings_Integrations_Honorific_ToggleHint));
|
||||||
|
internal static string Settings_Integrations_Honorific_Glow_Toggle => Get(nameof(Settings_Integrations_Honorific_Glow_Toggle));
|
||||||
|
internal static string Settings_Integrations_Honorific_Glow_Hint => Get(nameof(Settings_Integrations_Honorific_Glow_Hint));
|
||||||
internal static string Settings_Integrations_Honorific_LinkRepo => Get(nameof(Settings_Integrations_Honorific_LinkRepo));
|
internal static string Settings_Integrations_Honorific_LinkRepo => Get(nameof(Settings_Integrations_Honorific_LinkRepo));
|
||||||
internal static string Settings_Integrations_Honorific_LinkAuthor => Get(nameof(Settings_Integrations_Honorific_LinkAuthor));
|
internal static string Settings_Integrations_Honorific_LinkAuthor => Get(nameof(Settings_Integrations_Honorific_LinkAuthor));
|
||||||
internal static string Settings_Integrations_ComingSoon_SectionHeader => Get(nameof(Settings_Integrations_ComingSoon_SectionHeader));
|
internal static string Settings_Integrations_ComingSoon_SectionHeader => Get(nameof(Settings_Integrations_ComingSoon_SectionHeader));
|
||||||
@@ -390,4 +406,9 @@ internal class HellionStrings
|
|||||||
|
|
||||||
// Hellion Chat — v1.3.0 Honorific title slot tooltip
|
// Hellion Chat — v1.3.0 Honorific title slot tooltip
|
||||||
internal static string ChatHeader_HonorificTitle_Tooltip => Get(nameof(ChatHeader_HonorificTitle_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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,6 +383,36 @@
|
|||||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||||
<value>Als begrüßt markieren.</value>
|
<value>Als begrüßt markieren.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||||
|
<value>Tab anpinnen</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||||
|
<value>Tab lösen</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||||
|
<value>In Standard-Tab umwandeln</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||||
|
<value>Wandelt den TempTell in einen regulären Tab um. Die Tell-Bindung an die Person geht verloren, der Tab fängt dann Nachrichten anhand der Channel-Filter ein. Für „Tab überlebt Relog" stattdessen „Tab anpinnen" wählen.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||||
|
<value>Maximal {0} angepinnte Tell-Tabs erreicht. Erst einen lösen oder dauerhaft behalten.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||||
|
<value>Angepinnt — überlebt Relog.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinTooltip" xml:space="preserve">
|
||||||
|
<value>Angepinnte Tabs überleben Relog und behalten die Bindung an die Tell-Person.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_SectionHeader" xml:space="preserve">
|
||||||
|
<value>Angepinnt</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
|
||||||
|
<value>Sidebar-Breite</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
|
||||||
|
<value>Breite der Tab-Sidebar in Pixeln. Default (44 px) ist Icon-only; breiter machen damit Sektion-Header wie „Aktive Tells (3)" nicht abgeschnitten werden.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
|
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
|
||||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||||
@@ -398,7 +428,7 @@
|
|||||||
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||||
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell.</value>
|
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell. Diese Grenze gilt nur für den automatisch verwalteten Pool. Angepinnte Tell-Tabs (Rechtsklick → Tab anpinnen) leben in einem separaten Pool von bis zu 5 Tabs und überleben Relog.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||||
<value>Kompakte Anzeige</value>
|
<value>Kompakte Anzeige</value>
|
||||||
@@ -526,6 +556,14 @@
|
|||||||
<value>Emotes</value>
|
<value>Emotes</value>
|
||||||
</data>
|
</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 -->
|
<!-- Hellion Chat — Sektions-Überschriften des Database-Tabs -->
|
||||||
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
||||||
<value>Speicherung</value>
|
<value>Speicherung</value>
|
||||||
@@ -827,6 +865,12 @@
|
|||||||
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
||||||
<value>Zeigt deinen Custom-Titel aus Honorific im Header über dem Chat-Log an, in der von dir gewählten Farbe.</value>
|
<value>Zeigt deinen Custom-Titel aus Honorific im Header über dem Chat-Log an, in der von dir gewählten Farbe.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Toggle" xml:space="preserve">
|
||||||
|
<value>Glow-Outline rendern (Honorific)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Hint" xml:space="preserve">
|
||||||
|
<value>Kann die Framerate auf schwacher Hardware drücken. Rendert die Glow-Outline für Honorific-Titel, die sie nutzen. Gradient-Animation wird noch nicht unterstützt und wird stattdessen als Primärfarbe gezeichnet.</value>
|
||||||
|
</data>
|
||||||
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
||||||
<value>Honorific auf GitHub</value>
|
<value>Honorific auf GitHub</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -881,4 +925,13 @@
|
|||||||
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
||||||
<value>Custom-Titel von Honorific</value>
|
<value>Custom-Titel von Honorific</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|||||||
@@ -383,6 +383,36 @@
|
|||||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||||
<value>Mark as greeted.</value>
|
<value>Mark as greeted.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||||
|
<value>Pin Tab</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||||
|
<value>Unpin Tab</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||||
|
<value>Promote to permanent</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||||
|
<value>Turns this TempTell into a regular tab. The tell binding to the partner is dropped — the tab will catch messages by its channel filters from now on. For "tab survives relog while staying bound to this partner", use Pin Tab instead.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinTooltip" xml:space="preserve">
|
||||||
|
<value>Pinned tabs survive relog and stay bound to this conversation partner.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_SectionHeader" xml:space="preserve">
|
||||||
|
<value>Pinned</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
|
||||||
|
<value>Sidebar width</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
|
||||||
|
<value>Width of the tab sidebar in pixels. The default (44 px) is icon-only; widen it to fit the section headers like "Active Tells (3)" without truncation.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||||
|
<value>Maximum of {0} pinned tell tabs reached. Unpin one first, or use Promote to permanent.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||||
|
<value>Pinned — survives relog.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
|
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
|
||||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||||
@@ -398,7 +428,7 @@
|
|||||||
<value>Maximum number of auto-tell tabs</value>
|
<value>Maximum number of auto-tell tabs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||||
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell.</value>
|
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell. This limit applies to the auto-managed pool. Pinned tell tabs (right-click → Pin Tab) live in a separate pool of up to 5 and survive relog.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||||
<value>Compact display</value>
|
<value>Compact display</value>
|
||||||
@@ -526,6 +556,14 @@
|
|||||||
<value>Emotes</value>
|
<value>Emotes</value>
|
||||||
</data>
|
</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 -->
|
<!-- Hellion Chat — Database tab section headings -->
|
||||||
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
||||||
<value>Storage</value>
|
<value>Storage</value>
|
||||||
@@ -827,6 +865,12 @@
|
|||||||
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
||||||
<value>Shows your custom title from Honorific in the header above the chat log, in the colour you have chosen.</value>
|
<value>Shows your custom title from Honorific in the header above the chat log, in the colour you have chosen.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Toggle" xml:space="preserve">
|
||||||
|
<value>Render glow outlines (Honorific)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Hint" xml:space="preserve">
|
||||||
|
<value>May reduce frame rate on low-end hardware. Renders glow outlines for Honorific titles that use them. Gradient animation is not yet supported and will render as the primary colour.</value>
|
||||||
|
</data>
|
||||||
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
||||||
<value>Honorific on GitHub</value>
|
<value>Honorific on GitHub</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -881,4 +925,13 @@
|
|||||||
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
||||||
<value>Custom title from Honorific</value>
|
<value>Custom title from Honorific</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
using HellionChat.Themes.Builtin;
|
using HellionChat.Themes.Builtin;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Themes;
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
public sealed class ThemeRegistry
|
public sealed class ThemeRegistry
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<ThemeRegistry>? _logger;
|
||||||
|
|
||||||
public const string DefaultSlug = HellionArctic.Slug;
|
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> _builtIns;
|
||||||
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(
|
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(
|
||||||
StringComparer.OrdinalIgnoreCase
|
StringComparer.OrdinalIgnoreCase
|
||||||
@@ -13,8 +23,18 @@ public sealed class ThemeRegistry
|
|||||||
private readonly string? _customThemesDir;
|
private readonly string? _customThemesDir;
|
||||||
private Theme _active;
|
private Theme _active;
|
||||||
|
|
||||||
public ThemeRegistry(string? customThemesDir = null)
|
// 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, ILogger<ThemeRegistry>? logger = null)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
// Insertion order drives the Theme-Picker grid layout (3 columns).
|
// Insertion order drives the Theme-Picker grid layout (3 columns).
|
||||||
// Row 1: blue family. Row 2: purple to magenta family.
|
// Row 1: blue family. Row 2: purple to magenta family.
|
||||||
// Row 3: green / warm / classic. Row 4: Synthwave Sunset as a
|
// Row 3: green / warm / classic. Row 4: Synthwave Sunset as a
|
||||||
@@ -48,7 +68,9 @@ public sealed class ThemeRegistry
|
|||||||
if (_builtIns.TryGetValue(slug, out var b))
|
if (_builtIns.TryGetValue(slug, out var b))
|
||||||
return 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)
|
if (custom != null)
|
||||||
return custom;
|
return custom;
|
||||||
|
|
||||||
@@ -59,12 +81,70 @@ public sealed class ThemeRegistry
|
|||||||
|
|
||||||
public IEnumerable<Theme> AllCustom() => RefreshCustomCache();
|
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)
|
public void Switch(string slug)
|
||||||
{
|
{
|
||||||
var theme = Get(slug);
|
if (_builtIns.TryGetValue(slug, out var builtin))
|
||||||
// Defensive — ensures any future theme source always gets a populated cache.
|
{
|
||||||
theme.RecomputeAbgrCache();
|
_active = builtin;
|
||||||
_active = theme;
|
_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.
|
// 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION.
|
||||||
@@ -77,18 +157,30 @@ public sealed class ThemeRegistry
|
|||||||
return code == 0x80070020u || code == 0x80070021u;
|
return code == 0x80070020u || code == 0x80070021u;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom themes are loaded lazily, cached by LastWriteTime.
|
// Slug -> Theme lookup with the source path as an out-param so the
|
||||||
// A changed JSON is reloaded on the next lookup.
|
// Switch path can remember which file backs the active custom theme.
|
||||||
private Theme? LoadCustomBySlug(string slug)
|
// 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)
|
if (_customThemesDir is null)
|
||||||
return null;
|
return null;
|
||||||
if (!Directory.Exists(_customThemesDir))
|
if (!Directory.Exists(_customThemesDir))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
foreach (var theme in RefreshCustomCache())
|
foreach (var kvp in _customCache)
|
||||||
if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
{
|
||||||
return theme;
|
if (string.Equals(kvp.Value.Theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
sourcePath = kvp.Key;
|
||||||
|
return kvp.Value.Theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +210,7 @@ public sealed class ThemeRegistry
|
|||||||
catch (Exception ex) when (IsRecoverableFileLock(ex))
|
catch (Exception ex) when (IsRecoverableFileLock(ex))
|
||||||
{
|
{
|
||||||
// Editor mid-save: keep last known good, retry on next refresh.
|
// Editor mid-save: keep last known good, retry on next refresh.
|
||||||
Plugin.Log.Debug(
|
_logger?.LogDebug(
|
||||||
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
|
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
|
||||||
);
|
);
|
||||||
if (cached.Theme is not null)
|
if (cached.Theme is not 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+315
-50
@@ -22,6 +22,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using Lumina.Extensions;
|
using Lumina.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
private readonly CommandWrapper _clearHellionCommand;
|
private readonly CommandWrapper _clearHellionCommand;
|
||||||
private readonly CommandWrapper _hellionCommand;
|
private readonly CommandWrapper _hellionCommand;
|
||||||
|
private readonly SymbolPicker _symbolPicker;
|
||||||
|
|
||||||
internal bool ScreenshotMode;
|
internal bool ScreenshotMode;
|
||||||
private string Salt { get; }
|
private string Salt { get; }
|
||||||
@@ -97,10 +99,19 @@ public sealed class ChatLogWindow : Window
|
|||||||
private long FrameTime; // set every frame
|
private long FrameTime; // set every frame
|
||||||
internal long LastActivityTime = Environment.TickCount64;
|
internal long LastActivityTime = Environment.TickCount64;
|
||||||
|
|
||||||
internal ChatLogWindow(Plugin plugin)
|
private readonly ILogger<ChatLogWindow> _logger;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
||||||
|
internal ChatLogWindow(
|
||||||
|
Plugin plugin,
|
||||||
|
ILogger<ChatLogWindow> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
)
|
||||||
: base($"{Plugin.PluginName}###chat2")
|
: base($"{Plugin.PluginName}###chat2")
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
Salt = new Random().Next().ToString();
|
Salt = new Random().Next().ToString();
|
||||||
|
|
||||||
Size = new Vector2(500, 250);
|
Size = new Vector2(500, 250);
|
||||||
@@ -113,8 +124,10 @@ public sealed class ChatLogWindow : Window
|
|||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
// AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow.
|
// AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow.
|
||||||
|
|
||||||
PayloadHandler = new PayloadHandler(this);
|
PayloadHandler = new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>());
|
||||||
HandlerLender = new Lender<PayloadHandler>(() => new PayloadHandler(this));
|
HandlerLender = new Lender<PayloadHandler>(() =>
|
||||||
|
new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>())
|
||||||
|
);
|
||||||
|
|
||||||
SetUpTextCommandChannels();
|
SetUpTextCommandChannels();
|
||||||
SetUpAllCommands();
|
SetUpAllCommands();
|
||||||
@@ -129,6 +142,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
_clearHellionCommand.Execute += ClearLog;
|
_clearHellionCommand.Execute += ClearLog;
|
||||||
_hellionCommand.Execute += ToggleChat;
|
_hellionCommand.Execute += ToggleChat;
|
||||||
|
|
||||||
|
_symbolPicker = new SymbolPicker();
|
||||||
|
|
||||||
Plugin.ClientState.Login += Login;
|
Plugin.ClientState.Login += Login;
|
||||||
Plugin.ClientState.Logout += Logout;
|
Plugin.ClientState.Logout += Logout;
|
||||||
|
|
||||||
@@ -189,11 +204,28 @@ public sealed class ChatLogWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Cherry-picked from ChatTwo upstream ee7768ac (Infiziert90, 2026-05-16)
|
||||||
|
// - Replace the chat input when args.AddIfNotPresent / args.Input starts
|
||||||
|
// with a slash. Vanilla actions like the Friend List "/tell" entry and
|
||||||
|
// other plugins push slash commands through these args; appending them
|
||||||
|
// to existing text would produce inputs like "test/tell user@world".
|
||||||
|
// ---------------------------------------------------------------
|
||||||
if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent))
|
if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent))
|
||||||
Chat += args.AddIfNotPresent;
|
{
|
||||||
|
if (args.AddIfNotPresent.StartsWith('/'))
|
||||||
|
Chat = args.AddIfNotPresent;
|
||||||
|
else
|
||||||
|
Chat += args.AddIfNotPresent;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Input != null)
|
if (args.Input != null)
|
||||||
Chat += args.Input;
|
{
|
||||||
|
if (args.Input.StartsWith('/'))
|
||||||
|
Chat = args.Input;
|
||||||
|
else
|
||||||
|
Chat += args.Input;
|
||||||
|
}
|
||||||
|
|
||||||
var (info, reason, target) = (args.ChannelSwitchInfo, args.TellReason, args.TellTarget);
|
var (info, reason, target) = (args.ChannelSwitchInfo, args.TellReason, args.TellTarget);
|
||||||
|
|
||||||
@@ -277,7 +309,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
|| !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
|
|| !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -331,11 +363,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
case "hide":
|
case "hide":
|
||||||
CurrentHideState = HideState.User;
|
CurrentHideState = HideState.User;
|
||||||
Plugin.Log.Verbose("HideState: → User (chat hide command)");
|
_logger.LogTrace("HideState: → User (chat hide command)");
|
||||||
break;
|
break;
|
||||||
case "show":
|
case "show":
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose("HideState: → None (chat show command)");
|
_logger.LogTrace("HideState: → None (chat show command)");
|
||||||
break;
|
break;
|
||||||
case "toggle":
|
case "toggle":
|
||||||
CurrentHideState = CurrentHideState switch
|
CurrentHideState = CurrentHideState switch
|
||||||
@@ -345,7 +377,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
HideState.None => HideState.User,
|
HideState.None => HideState.User,
|
||||||
_ => CurrentHideState,
|
_ => CurrentHideState,
|
||||||
};
|
};
|
||||||
Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
_logger.LogTrace($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,8 +451,9 @@ public sealed class ChatLogWindow : Window
|
|||||||
// The hint banner renders before this block so ImGui already accounts for it.
|
// The hint banner renders before this block so ImGui already accounts for it.
|
||||||
height -= ImGui.GetFrameHeightWithSpacing();
|
height -= ImGui.GetFrameHeightWithSpacing();
|
||||||
|
|
||||||
// Status bar at the window bottom reserves 22px + 2px spacing.
|
// StatusBar.Height now bakes in its own DPI-aware 2px spacer, so the
|
||||||
height -= StatusBar.Height + 2;
|
// window reservation is just Height -- no extra +2 (v1.4.8 B1).
|
||||||
|
height -= StatusBar.Height;
|
||||||
|
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
@@ -441,11 +474,24 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
private void TabSwitched(Tab newTab, Tab previousTab)
|
private void TabSwitched(Tab newTab, Tab previousTab)
|
||||||
{
|
{
|
||||||
// Use the fixed channel if set by the user, or set it to the current tabs channel if this tab wasn't accessed before
|
// Use the fixed channel if set by the user. Otherwise, if the new tab
|
||||||
|
// has no channel state yet (fresh from JSON, never selected this
|
||||||
|
// session), seed from the previous tab — but deep-clone so we don't
|
||||||
|
// share TellTarget with the previous tab. Without the clone, a later
|
||||||
|
// /tell on the new tab would mutate the pinned tab's TellTarget and
|
||||||
|
// the Party/Linkshell channel would pop back to the pinned tell-mark.
|
||||||
if (newTab.Channel is not null)
|
if (newTab.Channel is not null)
|
||||||
|
{
|
||||||
newTab.CurrentChannel.Channel = newTab.Channel.Value;
|
newTab.CurrentChannel.Channel = newTab.Channel.Value;
|
||||||
|
}
|
||||||
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
|
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
|
||||||
newTab.CurrentChannel = previousTab.CurrentChannel;
|
{
|
||||||
|
newTab.CurrentChannel = previousTab.CurrentChannel.Clone();
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' "
|
||||||
|
+ $"(Channel={newTab.CurrentChannel.Channel}, TellTarget={newTab.CurrentChannel.TellTarget?.ToTargetString() ?? "null"})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
SetChannel(newTab.CurrentChannel.Channel);
|
SetChannel(newTab.CurrentChannel.Channel);
|
||||||
}
|
}
|
||||||
@@ -469,14 +515,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
Plugin.Log.Verbose("HideState: None → Battle");
|
_logger.LogTrace("HideState: None → Battle");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the chat is hidden because of battle, we reset it here
|
// If the chat is hidden because of battle, we reset it here
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose("HideState: Battle → None");
|
_logger.LogTrace("HideState: Battle → None");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||||
@@ -489,7 +535,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (Plugin.Functions.Chat.CheckHideFlags())
|
if (Plugin.Functions.Chat.CheckHideFlags())
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
Plugin.Log.Verbose("HideState: None → Cutscene");
|
_logger.LogTrace("HideState: None → Cutscene");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,7 +546,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
&& !Plugin.GposeActive
|
&& !Plugin.GposeActive
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
_logger.LogTrace($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,14 +554,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (CurrentHideState == HideState.Cutscene && Activate)
|
if (CurrentHideState == HideState.Cutscene && Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
_logger.LogTrace("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the user hid the chat and is now activating chat, reset the hide state
|
// if the user hid the chat and is now activating chat, reset the hide state
|
||||||
if (CurrentHideState == HideState.User && Activate)
|
if (CurrentHideState == HideState.User && Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose("HideState: User → None (activate)");
|
_logger.LogTrace("HideState: User → None (activate)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -622,6 +668,15 @@ public sealed class ChatLogWindow : Window
|
|||||||
IsOpen = true;
|
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()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
DrewThisFrame = true;
|
DrewThisFrame = true;
|
||||||
@@ -629,11 +684,15 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
DrawChatLog();
|
DrawChatLog();
|
||||||
AddPopOutsToDraw();
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error drawing Chat Log window");
|
_logger.LogError(ex, "Error drawing Chat Log window");
|
||||||
if (!NotifiedDrawFailure)
|
if (!NotifiedDrawFailure)
|
||||||
{
|
{
|
||||||
Plugin.Notification.AddNotification(
|
Plugin.Notification.AddNotification(
|
||||||
@@ -651,6 +710,13 @@ public sealed class ChatLogWindow : Window
|
|||||||
// input focus, which breaks every other ImGui window.
|
// input focus, which breaks every other ImGui window.
|
||||||
Activate = false;
|
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 =>
|
private static bool IsChatMode =>
|
||||||
@@ -666,18 +732,25 @@ public sealed class ChatLogWindow : Window
|
|||||||
LastWindowSize = currentSize;
|
LastWindowSize = currentSize;
|
||||||
LastWindowPos = ImGui.GetWindowPos();
|
LastWindowPos = ImGui.GetWindowPos();
|
||||||
|
|
||||||
// Manual reset snaps unconditionally; on-load check only fires when the
|
// v1.4.9 R2: skip the bounds-check chain on the first frame. The
|
||||||
// stored position has no overlap with any visible viewport.
|
// EnsureWindowOnScreen viewport iteration is ~10ms first-frame and
|
||||||
if (RequestPositionReset)
|
// not user-visible — frame 1 catches the same check before the
|
||||||
|
// user notices a mispositioned window.
|
||||||
|
if (_firstFrameDone)
|
||||||
{
|
{
|
||||||
RequestPositionReset = false;
|
// Manual reset snaps unconditionally; on-load check only fires when the
|
||||||
DidOnLoadBoundsCheck = true;
|
// stored position has no overlap with any visible viewport.
|
||||||
ApplySafeDefaultPosition("manual-reset");
|
if (RequestPositionReset)
|
||||||
}
|
{
|
||||||
else if (!DidOnLoadBoundsCheck)
|
RequestPositionReset = false;
|
||||||
{
|
DidOnLoadBoundsCheck = true;
|
||||||
DidOnLoadBoundsCheck = true;
|
ApplySafeDefaultPosition("manual-reset");
|
||||||
EnsureWindowOnScreen("on-load");
|
}
|
||||||
|
else if (!DidOnLoadBoundsCheck)
|
||||||
|
{
|
||||||
|
DidOnLoadBoundsCheck = true;
|
||||||
|
EnsureWindowOnScreen("on-load");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resized)
|
if (resized)
|
||||||
@@ -686,12 +759,17 @@ public sealed class ChatLogWindow : Window
|
|||||||
LastViewport = ImGui.GetWindowViewport().Handle;
|
LastViewport = ImGui.GetWindowViewport().Handle;
|
||||||
WasDocked = ImGui.IsWindowDocked();
|
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();
|
Plugin.InputPreview.CalculatePreview();
|
||||||
|
|
||||||
// Render the hint banner first so it sits above the tab area at full
|
// Render the hint banner first so it sits above the tab area at full
|
||||||
// window width. ImGui accounts for its height automatically.
|
// 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)
|
if (Plugin.Config.SidebarTabView)
|
||||||
DrawTabSidebar();
|
DrawTabSidebar();
|
||||||
@@ -746,6 +824,40 @@ public sealed class ChatLogWindow : Window
|
|||||||
)
|
)
|
||||||
inputColour = ecColour;
|
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 beforeIcon = ImGui.GetCursorPos();
|
||||||
|
|
||||||
var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
|
var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
|
||||||
@@ -924,7 +1036,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
// v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog,
|
// v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog,
|
||||||
// damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen.
|
// 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()
|
internal Dictionary<string, InputChannel> GetValidChannels()
|
||||||
@@ -975,6 +1091,16 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
private void DrawChannelName(Tab activeTab)
|
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);
|
var currentChannel = ReadChannelName(activeTab);
|
||||||
if (!currentChannel.SequenceEqual(PreviousChannel))
|
if (!currentChannel.SequenceEqual(PreviousChannel))
|
||||||
PreviousChannel = currentChannel;
|
PreviousChannel = currentChannel;
|
||||||
@@ -1608,7 +1734,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(ex, "Error drawing chat log");
|
_logger.LogWarning(ex, "Error drawing chat log");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1673,6 +1799,30 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.WantedTab = null;
|
Plugin.WantedTab = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sidebar render order: persistent tabs in their original Plugin.Config.Tabs
|
||||||
|
// position, then pinned TempTabs, then unpinned TempTabs. Returns indices
|
||||||
|
// into Plugin.Config.Tabs so tabI in the loop body still mirrors the real
|
||||||
|
// list position (LastTab / WantedTab stay consistent).
|
||||||
|
private static List<int> BuildSidebarRenderOrder()
|
||||||
|
{
|
||||||
|
var tabs = Plugin.Config.Tabs;
|
||||||
|
var persistent = new List<int>(tabs.Count);
|
||||||
|
var pinned = new List<int>();
|
||||||
|
var unpinned = new List<int>();
|
||||||
|
for (var i = 0; i < tabs.Count; i++)
|
||||||
|
{
|
||||||
|
if (TabLifecycleHelpers.IsInPinnedPool(tabs[i]))
|
||||||
|
pinned.Add(i);
|
||||||
|
else if (TabLifecycleHelpers.IsInUnpinnedPool(tabs[i]))
|
||||||
|
unpinned.Add(i);
|
||||||
|
else
|
||||||
|
persistent.Add(i);
|
||||||
|
}
|
||||||
|
persistent.AddRange(pinned);
|
||||||
|
persistent.AddRange(unpinned);
|
||||||
|
return persistent;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawTabSidebar()
|
private void DrawTabSidebar()
|
||||||
{
|
{
|
||||||
var currentTab = -1;
|
var currentTab = -1;
|
||||||
@@ -1685,7 +1835,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (!tabTable.Success)
|
if (!tabTable.Success)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, 44f);
|
var sidebarWidth = Math.Clamp(Plugin.Config.SidebarWidth, 44, 160);
|
||||||
|
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, sidebarWidth);
|
||||||
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
|
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
@@ -1704,23 +1855,42 @@ public sealed class ChatLogWindow : Window
|
|||||||
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
|
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
|
||||||
|
|
||||||
var previousTab = Plugin.CurrentTab;
|
var previousTab = Plugin.CurrentTab;
|
||||||
// Divider rendered once before the first temp tab with a live unit counter.
|
// Render order: persistent → pinned TempTabs → unpinned TempTabs.
|
||||||
|
// Underlying Plugin.Config.Tabs order is untouched (tabI mirrors
|
||||||
|
// the real list index), only the display sequence groups by
|
||||||
|
// section so each section can carry its own divider header.
|
||||||
|
var renderOrder = BuildSidebarRenderOrder();
|
||||||
|
var pinnedHeaderRendered = false;
|
||||||
var tempTabHeaderRendered = false;
|
var tempTabHeaderRendered = false;
|
||||||
var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
var pinnedCount = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||||
|
var unpinnedTempCount = Plugin.Config.Tabs.Count(
|
||||||
|
TabLifecycleHelpers.IsInUnpinnedPool
|
||||||
|
);
|
||||||
|
|
||||||
for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++)
|
foreach (var tabI in renderOrder)
|
||||||
{
|
{
|
||||||
var tab = Plugin.Config.Tabs[tabI];
|
var tab = Plugin.Config.Tabs[tabI];
|
||||||
if (tab.PopOut)
|
if (tab.PopOut)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (tab.IsTempTab && !tempTabHeaderRendered)
|
if (TabLifecycleHelpers.IsInPinnedPool(tab) && !pinnedHeaderRendered)
|
||||||
{
|
{
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||||
{
|
{
|
||||||
ImGui.TextDisabled(
|
ImGui.TextDisabled(
|
||||||
$"{HellionStrings.AutoTellTabs_SectionHeader} ({tempTabCount})"
|
$"{HellionStrings.PinTab_SectionHeader} ({pinnedCount})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pinnedHeaderRendered = true;
|
||||||
|
}
|
||||||
|
else if (TabLifecycleHelpers.IsInUnpinnedPool(tab) && !tempTabHeaderRendered)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled(
|
||||||
|
$"{HellionStrings.AutoTellTabs_SectionHeader} ({unpinnedTempCount})"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
tempTabHeaderRendered = true;
|
tempTabHeaderRendered = true;
|
||||||
@@ -1809,9 +1979,12 @@ public sealed class ChatLogWindow : Window
|
|||||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
|
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
|
||||||
using (Plugin.FontManager.FontAwesome.Push())
|
using (Plugin.FontManager.FontAwesome.Push())
|
||||||
{
|
{
|
||||||
|
// Button stretches with the configured sidebar width so a
|
||||||
|
// user-widened sidebar feels intentional, not a 36px icon
|
||||||
|
// floating in empty space.
|
||||||
clicked = ImGui.Button(
|
clicked = ImGui.Button(
|
||||||
$"{icon.ToIconString()}##sidebar-tab-{tabI}",
|
$"{icon.ToIconString()}##sidebar-tab-{tabI}",
|
||||||
new Vector2(36f, ImGui.GetFrameHeight())
|
new Vector2(sidebarWidth - 8f, ImGui.GetFrameHeight())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1871,11 +2044,35 @@ public sealed class ChatLogWindow : Window
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pin indicator: subtle thumbtack glyph top-left of the icon.
|
||||||
|
// Muted colour because the "Pinned" section header already
|
||||||
|
// groups these tabs visually — this is just a per-tab
|
||||||
|
// confirmation glyph, not the primary discoverability cue.
|
||||||
|
if (tab.IsPinned)
|
||||||
|
{
|
||||||
|
var min = ImGui.GetItemRectMin();
|
||||||
|
const float pinPadding = 1f;
|
||||||
|
var pinPos = new Vector2(min.X + pinPadding, min.Y + pinPadding);
|
||||||
|
var pinColor = theme.Colors.TextMuted;
|
||||||
|
// Dim further so the glyph reads as a hint, not a badge.
|
||||||
|
var pinAbgr = ColourUtil.RgbaToAbgr(pinColor) & 0x77FFFFFFu;
|
||||||
|
using (Plugin.FontManager.FontAwesome.Push())
|
||||||
|
{
|
||||||
|
ImGui
|
||||||
|
.GetWindowDrawList()
|
||||||
|
.AddText(pinPos, pinAbgr, FontAwesomeIcon.Thumbtack.ToIconString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tooltip mit Tab-Name + Unread-Counter beim Hover.
|
// Tooltip mit Tab-Name + Unread-Counter beim Hover.
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
{
|
{
|
||||||
using var tt = ImRaii.Tooltip();
|
using var tt = ImRaii.Tooltip();
|
||||||
ImGui.TextUnformatted($"{tab.Name}{unread}");
|
ImGui.TextUnformatted($"{tab.Name}{unread}");
|
||||||
|
if (tab.IsPinned)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(HellionStrings.PinTab_PinnedTooltip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawTabContextMenu(tab, tabI);
|
DrawTabContextMenu(tab, tabI);
|
||||||
@@ -1999,10 +2196,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
||||||
}
|
}
|
||||||
ImGui.SameLine(0f, gapAfterCrown);
|
ImGui.SameLine(0f, gapAfterCrown);
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
DrawHonorificTitleText(rendered, titleColor, title.Glow);
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(rendered);
|
|
||||||
}
|
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
@@ -2013,6 +2207,35 @@ public sealed class ChatLogWindow : Window
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renders the title text, optionally with a glow outline pre-pass. Glow is
|
||||||
|
// drawn at 8 cardinal offsets (±1 px) in the glow colour at reduced alpha,
|
||||||
|
// then the primary text on top. The pre-pass uses the window draw list so
|
||||||
|
// it composites correctly with the regular ImGui text that follows.
|
||||||
|
private void DrawHonorificTitleText(string rendered, Vector4 titleColor, Vector3? glow)
|
||||||
|
{
|
||||||
|
if (Plugin.Config.ShowHonorificGlow && glow is { } g)
|
||||||
|
{
|
||||||
|
var pos = ImGui.GetCursorScreenPos();
|
||||||
|
var glowColor = new Vector4(g.X, g.Y, g.Z, 0.4f);
|
||||||
|
var glowAbgr = ImGui.ColorConvertFloat4ToU32(glowColor);
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
for (var dy = -1; dy <= 1; dy++)
|
||||||
|
{
|
||||||
|
for (var dx = -1; dx <= 1; dx++)
|
||||||
|
{
|
||||||
|
if (dx == 0 && dy == 0)
|
||||||
|
continue;
|
||||||
|
drawList.AddText(new Vector2(pos.X + dx, pos.Y + dy), glowAbgr, rendered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(rendered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// One-time hint banner for the pop-out header button and right-click pathway.
|
// One-time hint banner for the pop-out header button and right-click pathway.
|
||||||
private float DrawV061HintBannerIfNeeded()
|
private float DrawV061HintBannerIfNeeded()
|
||||||
{
|
{
|
||||||
@@ -2059,7 +2282,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
Plugin.Config.SeenPopOutHeaderHint = true;
|
Plugin.Config.SeenPopOutHeaderHint = true;
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
Plugin.Log.Debug("v0.6.1 pop-out header hint dismissed");
|
_logger.LogDebug("v0.6.1 pop-out header hint dismissed");
|
||||||
if (openSettings)
|
if (openSettings)
|
||||||
Plugin.SettingsWindow.Toggle();
|
Plugin.SettingsWindow.Toggle();
|
||||||
}
|
}
|
||||||
@@ -2124,10 +2347,52 @@ public sealed class ChatLogWindow : Window
|
|||||||
anyChanged = true;
|
anyChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab.IsTempTab)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawPinControls(tab);
|
||||||
|
}
|
||||||
|
|
||||||
if (anyChanged)
|
if (anyChanged)
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawPinControls(Tab tab)
|
||||||
|
{
|
||||||
|
var svc = Plugin.AutoTellTabsService;
|
||||||
|
if (svc == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (tab.IsPinned)
|
||||||
|
{
|
||||||
|
if (ImGui.MenuItem(HellionStrings.PinTab_MenuUnpin))
|
||||||
|
{
|
||||||
|
svc.Unpin(tab);
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var atCap = svc.PinnedTempTabCount >= AutoTellTabsService.MaxPinnedTempTabs;
|
||||||
|
if (ImGui.MenuItem(HellionStrings.PinTab_MenuPin, enabled: !atCap))
|
||||||
|
{
|
||||||
|
if (svc.TryPin(tab))
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip(
|
||||||
|
atCap
|
||||||
|
? string.Format(
|
||||||
|
HellionStrings.PinTab_LimitReached,
|
||||||
|
AutoTellTabsService.MaxPinnedTempTabs
|
||||||
|
)
|
||||||
|
: HellionStrings.PinTab_PinTooltip
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly List<bool> PopOutDocked = [];
|
internal readonly List<bool> PopOutDocked = [];
|
||||||
internal readonly HashSet<Guid> PopOutWindows = [];
|
internal readonly HashSet<Guid> PopOutWindows = [];
|
||||||
|
|
||||||
@@ -2155,7 +2420,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (PopOutWindows.Contains(tab.Identifier))
|
if (PopOutWindows.Contains(tab.Identifier))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var window = new Popout(this, tab, i);
|
var window = new Popout(this, tab, i, _loggerFactory.CreateLogger<Popout>());
|
||||||
|
|
||||||
Plugin.WindowSystem.AddWindow(window);
|
Plugin.WindowSystem.AddWindow(window);
|
||||||
PopOutWindows.Add(tab.Identifier);
|
PopOutWindows.Add(tab.Identifier);
|
||||||
@@ -2672,7 +2937,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
var viewport = ImGui.GetMainViewport();
|
var viewport = ImGui.GetMainViewport();
|
||||||
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
||||||
Position = safePos;
|
Position = safePos;
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
|
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+80
-21
@@ -2,6 +2,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
@@ -16,6 +17,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Lumina.Data.Files;
|
using Lumina.Data.Files;
|
||||||
using Lumina.Text.ReadOnly;
|
using Lumina.Text.ReadOnly;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using MoreLinq;
|
using MoreLinq;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
@@ -33,11 +35,21 @@ public class DbViewer : Window
|
|||||||
|
|
||||||
private int CurrentPage = 1;
|
private int CurrentPage = 1;
|
||||||
private string SimpleSearchTerm = "";
|
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 bool OnlyCurrentCharacter = true;
|
||||||
private readonly Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels;
|
private readonly Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels;
|
||||||
|
|
||||||
private bool IsProcessing;
|
private bool IsProcessing;
|
||||||
private long ProcessingStart = Environment.TickCount64;
|
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 (DateTime Min, DateTime Max, int Page, bool Local, int ChannelCount) LastProcessed;
|
||||||
|
|
||||||
private string MinDateString = "";
|
private string MinDateString = "";
|
||||||
@@ -56,10 +68,13 @@ public class DbViewer : Window
|
|||||||
|
|
||||||
private bool NeedsScrollReset;
|
private bool NeedsScrollReset;
|
||||||
|
|
||||||
public DbViewer(Plugin plugin)
|
private readonly ILogger<DbViewer> _logger;
|
||||||
|
|
||||||
|
public DbViewer(Plugin plugin, ILogger<DbViewer> logger)
|
||||||
: base("DBViewer###chat2-dbviewer")
|
: base("DBViewer###chat2-dbviewer")
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
SelectedChannels = TabsUtil.MostlyPlayer;
|
SelectedChannels = TabsUtil.MostlyPlayer;
|
||||||
|
|
||||||
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
||||||
@@ -82,29 +97,13 @@ public class DbViewer : Window
|
|||||||
|
|
||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Plugin
|
|
||||||
.Commands.Register(
|
|
||||||
"/hellionView",
|
|
||||||
"Get access to your message history, with simple filter options.",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.Execute += Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin
|
// Slash-command tear-down moved to Plugin.TearDownCommands.
|
||||||
.Commands.Register(
|
|
||||||
"/hellionView",
|
|
||||||
"Get access to your message history, with simple filter options.",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.Execute -= Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Toggle(string _, string __) => Toggle();
|
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
|
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
|
||||||
@@ -233,6 +232,24 @@ public class DbViewer : Window
|
|||||||
tooltipRight: Language.Page_ArrowRight_Tooltip
|
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.SameLine(ImGui.GetContentRegionMax().X - width);
|
||||||
ImGui.SetNextItemWidth(width);
|
ImGui.SetNextItemWidth(width);
|
||||||
if (
|
if (
|
||||||
@@ -243,7 +260,7 @@ public class DbViewer : Window
|
|||||||
30
|
30
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Filtered = Filter(Messages);
|
TriggerFilterRefresh();
|
||||||
|
|
||||||
// Third row
|
// Third row
|
||||||
|
|
||||||
@@ -307,7 +324,7 @@ public class DbViewer : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Failed reading messages from database");
|
_logger.LogError(ex, "Failed reading messages from database");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -447,11 +464,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)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "FTS filter worker failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private ConcurrentStack<Message> Filter(Message[] messages)
|
private ConcurrentStack<Message> Filter(Message[] messages)
|
||||||
{
|
{
|
||||||
if (SimpleSearchTerm == "")
|
if (SimpleSearchTerm == "")
|
||||||
return new ConcurrentStack<Message>(messages.Reverse().OrderByDescending(m => m.Date));
|
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>(
|
return new ConcurrentStack<Message>(
|
||||||
messages
|
messages
|
||||||
.Reverse()
|
.Reverse()
|
||||||
@@ -570,7 +629,7 @@ public class DbViewer : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Failed creating txt backup");
|
_logger.LogError(ex, "Failed creating txt backup");
|
||||||
|
|
||||||
Notification.Content = "Error ...";
|
Notification.Content = "Error ...";
|
||||||
Notification.Type = NotificationType.Error;
|
Notification.Type = NotificationType.Error;
|
||||||
|
|||||||
@@ -28,17 +28,13 @@ public class DebuggerWindow : Window, IDisposable
|
|||||||
|
|
||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute += Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
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()
|
public override unsafe void Draw()
|
||||||
{
|
{
|
||||||
var agent = (nint)AgentItemDetail.Instance();
|
var agent = (nint)AgentItemDetail.Instance();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Dalamud.Bindings.ImGui;
|
|||||||
using Dalamud.Interface.Style;
|
using Dalamud.Interface.Style;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ internal class Popout : Window
|
|||||||
private readonly ChatLogWindow ChatLogWindow;
|
private readonly ChatLogWindow ChatLogWindow;
|
||||||
private readonly Tab Tab;
|
private readonly Tab Tab;
|
||||||
private readonly int Idx;
|
private readonly int Idx;
|
||||||
|
private readonly ILogger<Popout> _logger;
|
||||||
|
|
||||||
private long FrameTime;
|
private long FrameTime;
|
||||||
private long LastActivityTime = Environment.TickCount64;
|
private long LastActivityTime = Environment.TickCount64;
|
||||||
@@ -23,12 +25,13 @@ internal class Popout : Window
|
|||||||
// Exposed so AutoTellTabsService can locate this window during LRU eviction.
|
// Exposed so AutoTellTabsService can locate this window during LRU eviction.
|
||||||
internal Guid TabIdentifier => Tab.Identifier;
|
internal Guid TabIdentifier => Tab.Identifier;
|
||||||
|
|
||||||
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx)
|
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx, ILogger<Popout> logger)
|
||||||
: base($"{tab.Name}##popout")
|
: base($"{tab.Name}##popout")
|
||||||
{
|
{
|
||||||
ChatLogWindow = chatLogWindow;
|
ChatLogWindow = chatLogWindow;
|
||||||
Tab = tab;
|
Tab = tab;
|
||||||
Idx = idx;
|
Idx = idx;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
Size = new Vector2(350, 350);
|
Size = new Vector2(350, 350);
|
||||||
SizeCondition = ImGuiCond.FirstUseEver;
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
@@ -175,7 +178,7 @@ internal class Popout : Window
|
|||||||
{
|
{
|
||||||
Plugin.Config.SeenPopOutInputHint = true;
|
Plugin.Config.SeenPopOutInputHint = true;
|
||||||
ChatLogWindow.Plugin.SaveConfig();
|
ChatLogWindow.Plugin.SaveConfig();
|
||||||
Plugin.Log.Debug("Pop-Out input hint dismissed");
|
_logger.LogDebug("Pop-Out input hint dismissed");
|
||||||
if (openSettings)
|
if (openSettings)
|
||||||
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
||||||
}
|
}
|
||||||
@@ -214,13 +217,13 @@ internal class Popout : Window
|
|||||||
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: None -> Battle");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -232,7 +235,7 @@ internal class Popout : Window
|
|||||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +245,7 @@ internal class Popout : Window
|
|||||||
&& !Plugin.GposeActive
|
&& !Plugin.GposeActive
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Verbose(
|
_logger.LogTrace(
|
||||||
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
|
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
|
||||||
);
|
);
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
@@ -251,7 +254,7 @@ internal class Popout : Window
|
|||||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
Plugin.Log.Verbose(
|
_logger.LogTrace(
|
||||||
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
|
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -259,7 +262,7 @@ internal class Popout : Window
|
|||||||
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
||||||
}
|
}
|
||||||
|
|
||||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
|
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
|
||||||
|
|||||||
@@ -29,21 +29,13 @@ public class SeStringDebugger : Window
|
|||||||
|
|
||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
#if DEBUG
|
// Slash-command tear-down moved to Plugin.TearDownCommands.
|
||||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Toggle(string _, string __) => Toggle();
|
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
if (Plugin.MessageManager.LastMessage.Sender == null)
|
if (Plugin.MessageManager.LastMessage.Sender == null)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Dalamud.Utility;
|
|||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Ui.SettingsTabs;
|
using HellionChat.Ui.SettingsTabs;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
private SettingsView View = SettingsView.Overview;
|
private SettingsView View = SettingsView.Overview;
|
||||||
private readonly SettingsOverview Overview;
|
private readonly SettingsOverview Overview;
|
||||||
|
|
||||||
internal SettingsWindow(Plugin plugin)
|
internal SettingsWindow(Plugin plugin, ILoggerFactory loggerFactory)
|
||||||
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
||||||
{
|
{
|
||||||
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
|
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
|
||||||
@@ -45,13 +46,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
Tabs =
|
Tabs =
|
||||||
[
|
[
|
||||||
new General(Plugin, Mutable),
|
new General(Plugin, Mutable),
|
||||||
new ThemeAndLayout(Plugin, Mutable),
|
new ThemeAndLayout(Plugin, Mutable, loggerFactory.CreateLogger<ThemeAndLayout>()),
|
||||||
new FontsAndColours(Plugin, Mutable),
|
new FontsAndColours(Plugin, Mutable, loggerFactory.CreateLogger<FontsAndColours>()),
|
||||||
new SettingsTabs.Window(Plugin, Mutable),
|
new SettingsTabs.Window(Plugin, Mutable),
|
||||||
new Chat(Plugin, Mutable),
|
new Chat(Plugin, Mutable),
|
||||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||||
new SettingsTabs.Privacy(Plugin, Mutable),
|
new SettingsTabs.Privacy(Plugin, Mutable),
|
||||||
new DataManagement(Plugin, Mutable),
|
new DataManagement(Plugin, Mutable, loggerFactory.CreateLogger<DataManagement>()),
|
||||||
new SettingsTabs.Integrations(Plugin, Mutable),
|
new SettingsTabs.Integrations(Plugin, Mutable),
|
||||||
new Information(Mutable),
|
new Information(Mutable),
|
||||||
];
|
];
|
||||||
@@ -60,23 +61,11 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Initialise();
|
Initialise();
|
||||||
|
|
||||||
Plugin
|
|
||||||
.Commands.Register("/hellion", "Perform various actions with Hellion Chat.")
|
|
||||||
.Execute += Command;
|
|
||||||
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
|
// Slash-command + OpenConfigUi tear-down moved to Plugin.TearDownCommands.
|
||||||
Plugin.Commands.Register("/hellion").Execute -= Command;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Command(string command, string args)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(args))
|
|
||||||
Toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Initialise()
|
private void Initialise()
|
||||||
|
|||||||
@@ -139,6 +139,12 @@ internal sealed class Chat : ISettingsTab
|
|||||||
);
|
);
|
||||||
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMsgUniqueLink_Description);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using HellionChat.Export;
|
|||||||
using HellionChat.Privacy;
|
using HellionChat.Privacy;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui.SettingsTabs;
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
{
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
|
private readonly ILogger<DataManagement> _logger;
|
||||||
|
|
||||||
public string Name =>
|
public string Name =>
|
||||||
HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement";
|
HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement";
|
||||||
@@ -136,10 +138,11 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
internal DataManagement(Plugin plugin, Configuration mutable)
|
internal DataManagement(Plugin plugin, Configuration mutable, ILogger<DataManagement> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
@@ -229,7 +232,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Unable to delete old database");
|
_logger.LogError(e, "Unable to delete old database");
|
||||||
WrapperUtil.AddNotification(
|
WrapperUtil.AddNotification(
|
||||||
Language.Options_Database_Old_Delete_Error,
|
Language.Options_Database_Old_Delete_Error,
|
||||||
NotificationType.Error
|
NotificationType.Error
|
||||||
@@ -391,7 +394,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
|
|
||||||
Plugin.Log.Information($"Manual retention run deleted {deleted} expired messages.");
|
_logger.LogInformation($"Manual retention run deleted {deleted} expired messages.");
|
||||||
|
|
||||||
if (deleted > 0)
|
if (deleted > 0)
|
||||||
{
|
{
|
||||||
@@ -405,7 +408,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
.Wait(TimeSpan.FromSeconds(5))
|
.Wait(TimeSpan.FromSeconds(5))
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
"Retention sweep: framework refresh timed out after 5s."
|
"Retention sweep: framework refresh timed out after 5s."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -418,7 +421,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Manual retention run failed");
|
_logger.LogError(e, "Manual retention run failed");
|
||||||
WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -566,7 +569,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Failed to compute cleanup preview");
|
_logger.LogError(e, "Failed to compute cleanup preview");
|
||||||
WrapperUtil.AddNotification(
|
WrapperUtil.AddNotification(
|
||||||
HellionStrings.Cleanup_PreviewError,
|
HellionStrings.Cleanup_PreviewError,
|
||||||
NotificationType.Error
|
NotificationType.Error
|
||||||
@@ -587,7 +590,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
||||||
Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages");
|
_logger.LogInformation($"Privacy cleanup: deleted {deleted} messages");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!Plugin
|
!Plugin
|
||||||
@@ -599,7 +602,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
.Wait(TimeSpan.FromSeconds(5))
|
.Wait(TimeSpan.FromSeconds(5))
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
_logger.LogWarning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapperUtil.AddNotification(
|
WrapperUtil.AddNotification(
|
||||||
@@ -609,7 +612,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Privacy cleanup failed");
|
_logger.LogError(e, "Privacy cleanup failed");
|
||||||
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -769,7 +772,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Export failed");
|
_logger.LogError(e, "Export failed");
|
||||||
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -849,7 +852,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Clearing messages from database");
|
_logger.LogWarning("Clearing messages from database");
|
||||||
Plugin.MessageManager.Store.ClearMessages();
|
Plugin.MessageManager.Store.ClearMessages();
|
||||||
Plugin.MessageManager.ClearAllTabs();
|
Plugin.MessageManager.ClearAllTabs();
|
||||||
|
|
||||||
@@ -907,7 +910,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
|
|
||||||
private void InsertMessages(int count)
|
private void InsertMessages(int count)
|
||||||
{
|
{
|
||||||
Plugin.Log.Info($"Inserting {count} messages due to user request");
|
_logger.LogInformation($"Inserting {count} messages due to user request");
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
var playerName = Plugin.PlayerState.CharacterName;
|
var playerName = Plugin.PlayerState.CharacterName;
|
||||||
@@ -952,7 +955,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
|
|
||||||
var elapsedTicks = stopwatch.ElapsedTicks;
|
var elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -962,7 +965,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
|
|
||||||
elapsedTicks = stopwatch.ElapsedTicks;
|
elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -973,7 +976,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
Plugin.MessageManager.ClearAllTabs();
|
Plugin.MessageManager.ClearAllTabs();
|
||||||
elapsedTicks = stopwatch.ElapsedTicks;
|
elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -986,7 +989,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
Plugin.MessageManager.FilterAllTabs();
|
Plugin.MessageManager.FilterAllTabs();
|
||||||
elapsedTicks = stopwatch.ElapsedTicks;
|
elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui.SettingsTabs;
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
@@ -14,14 +15,16 @@ internal sealed class FontsAndColours : ISettingsTab
|
|||||||
{
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
|
private readonly ILogger<FontsAndColours> _logger;
|
||||||
|
|
||||||
public string Name =>
|
public string Name =>
|
||||||
HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours";
|
HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours";
|
||||||
|
|
||||||
internal FontsAndColours(Plugin plugin, Configuration mutable)
|
internal FontsAndColours(Plugin plugin, Configuration mutable, ILogger<FontsAndColours> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
@@ -312,6 +315,6 @@ internal sealed class FontsAndColours : ISettingsTab
|
|||||||
}
|
}
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
GlobalParametersCache.Refresh();
|
GlobalParametersCache.Refresh();
|
||||||
Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
_logger.LogDebug($"Applied chat colour preset: {preset.DisplayName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ internal sealed class Integrations : ISettingsTab
|
|||||||
{
|
{
|
||||||
ImGui.TextWrapped(HellionStrings.Settings_Integrations_Honorific_ToggleHint);
|
ImGui.TextWrapped(HellionStrings.Settings_Integrations_Honorific_ToggleHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
ImGui.Checkbox(
|
||||||
|
HellionStrings.Settings_Integrations_Honorific_Glow_Toggle,
|
||||||
|
ref Mutable.ShowHonorificGlow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Integrations_Honorific_Glow_Hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Honorific has no LICENSE in its repo so we link upstream and author
|
// Honorific has no LICENSE in its repo so we link upstream and author
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Themes;
|
using HellionChat.Themes;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui.SettingsTabs;
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
@@ -11,16 +12,18 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
|||||||
{
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
|
private readonly ILogger<ThemeAndLayout> _logger;
|
||||||
|
|
||||||
private string? _applyDismissedFor;
|
private string? _applyDismissedFor;
|
||||||
|
|
||||||
public string Name =>
|
public string Name =>
|
||||||
HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout";
|
HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout";
|
||||||
|
|
||||||
internal ThemeAndLayout(Plugin plugin, Configuration mutable)
|
internal ThemeAndLayout(Plugin plugin, Configuration mutable, ILogger<ThemeAndLayout> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
@@ -90,7 +93,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
|||||||
var path = Path.Combine(dir, fileName);
|
var path = Path.Combine(dir, fileName);
|
||||||
var json = ThemeJsonWriter.Serialize(active);
|
var json = ThemeJsonWriter.Serialize(active);
|
||||||
File.WriteAllText(path, json);
|
File.WriteAllText(path, json);
|
||||||
Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}");
|
_logger.LogInformation($"Exported active theme '{active.Slug}' to {path}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,6 +253,26 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
|||||||
string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)
|
string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Mutable.SidebarTabView)
|
||||||
|
{
|
||||||
|
var sidebarWidth = Mutable.SidebarWidth;
|
||||||
|
if (
|
||||||
|
ImGui.SliderInt(
|
||||||
|
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Name,
|
||||||
|
ref sidebarWidth,
|
||||||
|
44,
|
||||||
|
160,
|
||||||
|
$"{sidebarWidth} px"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Mutable.SidebarWidth = sidebarWidth;
|
||||||
|
}
|
||||||
|
ImGuiUtil.HelpMarker(
|
||||||
|
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Globalization;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
@@ -9,12 +10,20 @@ using HellionChat.Util;
|
|||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
// Bottom status bar, 22px tall. Slots left to right: channel indicator,
|
// Bottom status bar. Slots left to right: channel indicator, privacy badge,
|
||||||
// privacy badge, counts, tells (hidden at 0), version (right-aligned).
|
// counts, tells (hidden at 0), version (right-aligned). Updates at 1Hz;
|
||||||
// Updates at 1Hz; format strings are cached between updates.
|
// format strings are cached between updates.
|
||||||
internal sealed class StatusBar
|
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;
|
private const long UpdateIntervalMs = 1000;
|
||||||
|
|
||||||
// Initially outdated so the first frame always computes fresh.
|
// 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();
|
var sw = Stopwatch.StartNew();
|
||||||
AllEntries();
|
AllEntries();
|
||||||
Plugin.Log.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,
|
IsBackground = true,
|
||||||
@@ -197,7 +202,7 @@ internal static class AutoTranslate
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, $"failed to translate: {lookup}");
|
Plugin.LogProxy.Error(ex, $"failed to translate: {lookup}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace HellionChat.Util;
|
||||||
|
|
||||||
|
internal sealed class DalamudPluginLogProxy : IPluginLogProxy
|
||||||
|
{
|
||||||
|
private readonly IPluginLog _log;
|
||||||
|
|
||||||
|
public DalamudPluginLogProxy(IPluginLog log) => _log = log;
|
||||||
|
|
||||||
|
public void Verbose(string message) => _log.Verbose(message);
|
||||||
|
|
||||||
|
public void Verbose(Exception exception, string message) => _log.Verbose(exception, message);
|
||||||
|
|
||||||
|
public void Verbose(string messageTemplate, params object[] values) =>
|
||||||
|
_log.Verbose(messageTemplate, values);
|
||||||
|
|
||||||
|
public void Debug(string message) => _log.Debug(message);
|
||||||
|
|
||||||
|
public void Debug(Exception exception, string message) => _log.Debug(exception, message);
|
||||||
|
|
||||||
|
public void Debug(string messageTemplate, params object[] values) =>
|
||||||
|
_log.Debug(messageTemplate, values);
|
||||||
|
|
||||||
|
public void Information(string message) => _log.Information(message);
|
||||||
|
|
||||||
|
public void Information(Exception exception, string message) =>
|
||||||
|
_log.Information(exception, message);
|
||||||
|
|
||||||
|
public void Information(string messageTemplate, params object[] values) =>
|
||||||
|
_log.Information(messageTemplate, values);
|
||||||
|
|
||||||
|
public void Info(string message) => _log.Info(message);
|
||||||
|
|
||||||
|
public void Info(Exception exception, string message) => _log.Info(exception, message);
|
||||||
|
|
||||||
|
public void Info(string messageTemplate, params object[] values) =>
|
||||||
|
_log.Info(messageTemplate, values);
|
||||||
|
|
||||||
|
public void Warning(string message) => _log.Warning(message);
|
||||||
|
|
||||||
|
public void Warning(Exception exception, string message) => _log.Warning(exception, message);
|
||||||
|
|
||||||
|
public void Warning(string messageTemplate, params object[] values) =>
|
||||||
|
_log.Warning(messageTemplate, values);
|
||||||
|
|
||||||
|
public void Error(string message) => _log.Error(message);
|
||||||
|
|
||||||
|
public void Error(Exception exception, string message) => _log.Error(exception, message);
|
||||||
|
|
||||||
|
public void Error(string messageTemplate, params object[] values) =>
|
||||||
|
_log.Error(messageTemplate, values);
|
||||||
|
|
||||||
|
public void Fatal(string message) => _log.Fatal(message);
|
||||||
|
|
||||||
|
public void Fatal(Exception exception, string message) => _log.Fatal(exception, message);
|
||||||
|
|
||||||
|
public void Fatal(string messageTemplate, params object[] values) =>
|
||||||
|
_log.Fatal(messageTemplate, values);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace HellionChat.Util;
|
||||||
|
|
||||||
|
// Plugin.LogProxy bridge for consumers that cannot take a logger via the
|
||||||
|
// constructor: static helpers (EmoteCache et al.), Dalamud-reflected types
|
||||||
|
// (Configuration), data classes with mass instantiation (Message) and
|
||||||
|
// instance classes that only log from static methods (FontManager).
|
||||||
|
internal interface IPluginLogProxy
|
||||||
|
{
|
||||||
|
void Verbose(string message);
|
||||||
|
void Verbose(Exception exception, string message);
|
||||||
|
void Verbose(string messageTemplate, params object[] values);
|
||||||
|
|
||||||
|
void Debug(string message);
|
||||||
|
void Debug(Exception exception, string message);
|
||||||
|
void Debug(string messageTemplate, params object[] values);
|
||||||
|
|
||||||
|
void Information(string message);
|
||||||
|
void Information(Exception exception, string message);
|
||||||
|
void Information(string messageTemplate, params object[] values);
|
||||||
|
|
||||||
|
// IPluginLog exposes Info as a distinct method (short alias of
|
||||||
|
// Information) — both are present so call-sites stay drop-in.
|
||||||
|
void Info(string message);
|
||||||
|
void Info(Exception exception, string message);
|
||||||
|
void Info(string messageTemplate, params object[] values);
|
||||||
|
|
||||||
|
void Warning(string message);
|
||||||
|
void Warning(Exception exception, string message);
|
||||||
|
void Warning(string messageTemplate, params object[] values);
|
||||||
|
|
||||||
|
void Error(string message);
|
||||||
|
void Error(Exception exception, string message);
|
||||||
|
void Error(string messageTemplate, params object[] values);
|
||||||
|
|
||||||
|
void Fatal(string message);
|
||||||
|
void Fatal(Exception exception, string message);
|
||||||
|
void Fatal(string messageTemplate, params object[] values);
|
||||||
|
}
|
||||||
@@ -583,7 +583,9 @@ internal static class ImGuiUtil
|
|||||||
|
|
||||||
using (ImRaii.Disabled(isMax))
|
using (ImRaii.Disabled(isMax))
|
||||||
{
|
{
|
||||||
if (IconButton(FontAwesomeIcon.ArrowRight, id + 1.ToString()))
|
// Parentheses pin the operator precedence: without them this resolves as
|
||||||
|
// id.ToString() + "1" (e.g. "01" instead of "1").
|
||||||
|
if (IconButton(FontAwesomeIcon.ArrowRight, (id + 1).ToString()))
|
||||||
selected++;
|
selected++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ public static class MemoryUtil
|
|||||||
str.Append(' ');
|
str.Append(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Log.Information(str.ToString());
|
Plugin.LogProxy.Information(str.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace HellionChat.Util;
|
||||||
|
|
||||||
|
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
|
||||||
|
// sites in Plugin.cs and Configuration.cs so they stay in lockstep — a
|
||||||
|
// load-time strip that disagrees with the save-time strip is exactly how
|
||||||
|
// pinned tabs would silently fall out of the JSON.
|
||||||
|
internal static class TabLifecycleHelpers
|
||||||
|
{
|
||||||
|
public static bool IsInUnpinnedPool(Tab t) => t.IsTempTab && !t.IsPinned;
|
||||||
|
|
||||||
|
public static bool IsInPinnedPool(Tab t) => t.IsTempTab && t.IsPinned;
|
||||||
|
|
||||||
|
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
|
||||||
|
|
||||||
|
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
|
||||||
|
}
|
||||||
@@ -21,12 +21,12 @@ public static class WrapperUtil
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Plugin.Log.Debug($"Opening URI {uri} in default browser");
|
Plugin.LogProxy.Debug($"Opening URI {uri} in default browser");
|
||||||
Plugin.PlatformUtil.OpenLink(uri.ToString());
|
Plugin.PlatformUtil.OpenLink(uri.ToString());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error($"Error opening URI: {ex}");
|
Plugin.LogProxy.Error($"Error opening URI: {ex}");
|
||||||
AddNotification(Language.Context_OpenInBrowserError, NotificationType.Error);
|
AddNotification(Language.Context_OpenInBrowserError, NotificationType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+395
-107
@@ -1,110 +1,398 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net10.0-windows7.0": {
|
"net10.0-windows7.0": {
|
||||||
"DalamudPackager": {
|
"DalamudPackager": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[15.0.0, )",
|
"requested": "[15.0.0, )",
|
||||||
"resolved": "15.0.0",
|
"resolved": "15.0.0",
|
||||||
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
||||||
},
|
},
|
||||||
"DotNet.ReproducibleBuilds": {
|
"DotNet.ReproducibleBuilds": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[1.2.39, )",
|
"requested": "[1.2.39, )",
|
||||||
"resolved": "1.2.39",
|
"resolved": "1.2.39",
|
||||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||||
},
|
},
|
||||||
"MessagePack": {
|
"MessagePack": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.1.4, 4.0.0)",
|
"requested": "[3.1.4, 4.0.0)",
|
||||||
"resolved": "3.1.4",
|
"resolved": "3.1.4",
|
||||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"MessagePack.Annotations": "3.1.4",
|
"MessagePack.Annotations": "3.1.4",
|
||||||
"MessagePackAnalyzer": "3.1.4",
|
"MessagePackAnalyzer": "3.1.4",
|
||||||
"Microsoft.NET.StringTools": "17.11.4"
|
"Microsoft.NET.StringTools": "17.11.4"
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Data.Sqlite": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[10.0.7, )",
|
|
||||||
"resolved": "10.0.7",
|
|
||||||
"contentHash": "DZ6G2QuyPrsh5VS+wfiZbNBtYT6p+CkxXjD0aZHF04xso7QsG/uk0JpG30hzYlK6u/wtTzta1Dqfgbc/Sl2sDA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Data.Sqlite.Core": "10.0.7",
|
|
||||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
|
|
||||||
"SQLitePCLRaw.core": "2.1.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"morelinq": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[4.4.0, )",
|
|
||||||
"resolved": "4.4.0",
|
|
||||||
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
|
|
||||||
},
|
|
||||||
"Pidgin": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[3.5.1, 4.0.0)",
|
|
||||||
"resolved": "3.5.1",
|
|
||||||
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
|
|
||||||
},
|
|
||||||
"SixLabors.ImageSharp": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[3.1.12, 4.0.0)",
|
|
||||||
"resolved": "3.1.12",
|
|
||||||
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
|
||||||
},
|
|
||||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[3.50.3, )",
|
|
||||||
"resolved": "3.50.3",
|
|
||||||
"contentHash": "tVyhqQ8wxgedWiiPFChyZhE8I3PkOM/AE1azsj1qsdYUws13ONBFyi3aDxju4tD2kzedB2q5+50WrTyY0h2gMQ=="
|
|
||||||
},
|
|
||||||
"MessagePack.Annotations": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.1.4",
|
|
||||||
"contentHash": "aVWrDAkCdqxwQsz/q0ldPh2EFn48M99YUzE9OvZjMq2RNLKz4o2z88iGFvSvbMqOWRweRvKPHBJZe22PRqzslQ=="
|
|
||||||
},
|
|
||||||
"MessagePackAnalyzer": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.1.4",
|
|
||||||
"contentHash": "CTaSsN/liJ7MhLCAB7Z4ZLBNuVGCq9lt2BT/cbrc9vzGv89yK3CqIA+z9T19a11eQYl9etZHL6MQJgCqECRVpg=="
|
|
||||||
},
|
|
||||||
"Microsoft.Data.Sqlite.Core": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "10.0.7",
|
|
||||||
"contentHash": "xVrtBg3M1wJlBDkoT0dXEYB/wSc8bIHJPYtw/bu1AqpWgF79uPSs87DAhERR/Ilumre6TKZa1cjMg3VUUObVLA==",
|
|
||||||
"dependencies": {
|
|
||||||
"SQLitePCLRaw.core": "2.1.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.NET.StringTools": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "17.11.4",
|
|
||||||
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
|
||||||
},
|
|
||||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.11",
|
|
||||||
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
|
|
||||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SQLitePCLRaw.core": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.11",
|
|
||||||
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
|
|
||||||
},
|
|
||||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.11",
|
|
||||||
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
|
|
||||||
"dependencies": {
|
|
||||||
"SQLitePCLRaw.core": "2.1.11"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Data.Sqlite": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.7, )",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "DZ6G2QuyPrsh5VS+wfiZbNBtYT6p+CkxXjD0aZHF04xso7QsG/uk0JpG30hzYlK6u/wtTzta1Dqfgbc/Sl2sDA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Data.Sqlite.Core": "10.0.7",
|
||||||
|
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
|
||||||
|
"SQLitePCLRaw.core": "2.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.7, 11.0.0)",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "91F/o3emPV/+xY/ip3s2LqDNF14kjttlVtq0BXgg6p4MnCzeSZxnUJm+t6WRrtD3JdGo88/oX+z7OwK4y8PZuw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Hosting": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.7, 11.0.0)",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "M/vBpfWcschvS2EUeq7cHfscsxabiGTptXwV7GeSueovGiSoNjyo1j5PMcWuOAAQrRW3nRqxZk8NeumrmpzUBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Binder": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.CommandLine": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Json": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.UserSecrets": "10.0.7",
|
||||||
|
"Microsoft.Extensions.DependencyInjection": "10.0.7",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Diagnostics": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Physical": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Hosting.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Console": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Debug": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.EventLog": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.EventSource": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.7, 11.0.0)",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "hOeRIQ63GkgiYCB/MIFp+LQs8aXpJXpB55t6Aj37ab7t2/6WeFcPXxYM9hdy/o5tffzwf8mhqzLJP6mjGYCxjw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Options": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.7, 11.0.0)",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "00SHUGTh2jSMvIr6x9Xwd2nE+B5/qFCO/9hDwUDhJsjYRDlADmaBZ7tqehXzBDsfjHSXJzuRHJzPYPPjphBQ7Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"morelinq": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[4.4.0, )",
|
||||||
|
"resolved": "4.4.0",
|
||||||
|
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
|
||||||
|
},
|
||||||
|
"Pidgin": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.5.1, 4.0.0)",
|
||||||
|
"resolved": "3.5.1",
|
||||||
|
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
|
||||||
|
},
|
||||||
|
"SixLabors.ImageSharp": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.1.12, 4.0.0)",
|
||||||
|
"resolved": "3.1.12",
|
||||||
|
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.50.3, )",
|
||||||
|
"resolved": "3.50.3",
|
||||||
|
"contentHash": "tVyhqQ8wxgedWiiPFChyZhE8I3PkOM/AE1azsj1qsdYUws13ONBFyi3aDxju4tD2kzedB2q5+50WrTyY0h2gMQ=="
|
||||||
|
},
|
||||||
|
"MessagePack.Annotations": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.1.4",
|
||||||
|
"contentHash": "aVWrDAkCdqxwQsz/q0ldPh2EFn48M99YUzE9OvZjMq2RNLKz4o2z88iGFvSvbMqOWRweRvKPHBJZe22PRqzslQ=="
|
||||||
|
},
|
||||||
|
"MessagePackAnalyzer": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.1.4",
|
||||||
|
"contentHash": "CTaSsN/liJ7MhLCAB7Z4ZLBNuVGCq9lt2BT/cbrc9vzGv89yK3CqIA+z9T19a11eQYl9etZHL6MQJgCqECRVpg=="
|
||||||
|
},
|
||||||
|
"Microsoft.Data.Sqlite.Core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "xVrtBg3M1wJlBDkoT0dXEYB/wSc8bIHJPYtw/bu1AqpWgF79uPSs87DAhERR/Ilumre6TKZa1cjMg3VUUObVLA==",
|
||||||
|
"dependencies": {
|
||||||
|
"SQLitePCLRaw.core": "2.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "wZbGh7J8R1vXN525O6d8dlcDTxhRTnd5MyW4LdfP5S0tSnTwTCseYSrq6g0Mxh7W9xn8P/2xPuf0D/m6k2dy2w==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "t56nEgvECcyLPojZIUFWJknQQDAbgfTf9J+QMYJE1YYvVgz69vN6B/AKL8Grvj3Lcnp8kTpNqwmwFhb3YLJmtQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.Binder": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "8bS1qIaRivny+WX+49pmeJ6iAylbtX8C0DLEcCQWZjdxQvLqaMssXiGD9P/6pYElrHbK5/nAHmjbQ8STqdMYeg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.CommandLine": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "3lNjglxfFxOzI9zG+3HSg/YSGqo//8Fqw6u6iuIamZb4JCorbA3JLaeWOpfKTAPi2UJwaispOXWx14dUqcGz4A==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "TWto3imA+mJMLZI+5sbgLiFFoOFNFkizQYNaC5jTuiHKn3diwm1RN7mWDOEZN9kG2bixw7IvgpvtUG5/teSRzA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.FileExtensions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "qbZLvLsoTdArSloEnSxs21P781YUmwVmHc5NJPQD/ezAreQ7884z+6QfAZVKi86WAZtzx83jK2uC4itxOM44gQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Physical": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.Json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "64dimvyyKk0dbUbrLg/YCv4ugJ4sVz2aXLwfvZwR1EC4tJqW9ru/oVRcXwoJRa2lQGXtYtlpk4maWOeIb48tQw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.UserSecrets": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "YqVIICoIdl0016wkeO2WQS+uEbEXbUhMLKdC5rZNl1X3nu59F+nwaAHdHjq/4OK+Cx31DYmNUSFh+MUot8qSDw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Json": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Physical": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "Z6mfFEaFcwCfSboxJwOLfu7/31npCY9q70WUamHW/vRQhDvBKOT4Vf9YkZj5J6hLvJpb0oDEYfHunQZj0xxvKw=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "l+smp1qPlU0OUXD0OGfdp7OUFrbdq7ZaP5T7m2WpfZ4RFKD7iG73BAT7tjSMxNmbSXkhAn1jYHOAqzYG1r9sNg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "uJ9JP677y+uy+C0vtaSfi7XXgFAdz8DhU3M9lwwIXDfQKcyQ0yxM9DVYa0NXDtdVTYA2eBUtVFZ8LY0GCdeE/w==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "teioDgVpi8L186wUfrXQV1YuBt6lCSPmFZiMZo53+FZxHFjOV+f4GXo4LXgJ273Mku9//AdXWVjk9J7eJP6inw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.FileProviders.Physical": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "zhgWg/i0ECj5v0jLFBSZHplvc5ygCI91DR4nne+BP4XAKF5ycz0pEKnFiTw8C1jCABJEZsnBZh6pXAvn71kFmw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileSystemGlobbing": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.FileSystemGlobbing": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "NTUspqB+vH9g4wAD6KPOBx01xqYuKXR/cHXm449zpbq1GqfjdAxBmg7eJXrNsPw7SKwIdT2cJ05GxYVvc+lvsA=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Hosting.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "5s8d6qC6EA8UOI4wR/+zlsq7SXttJMRb9d7zvVZ7+bE3CQEfVtC9ITUDCommm87R1zzj6WJBbCnztuIJXnP3DA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Configuration": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "7BBnoGF37USiu7j434put9mDp7EjdlNDIZsR4vHfC1FbLZeLqiWjgJbeEtF0p59Ryqt8AtraHawf0ZKbe5jibg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Binder": "10.0.7",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Console": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "DA++Es6v6W0HfrOrw+K8WyN6jNnZHp640PDdEvl8yfeVmgflKdn6vSSFvufNUSOuY+M2ZaSUgfY+jUKtNpXcCw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Configuration": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Debug": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "Y6DSt/JZApunYWKqTtqbdsR6iqAvHx3D0tavbNJ1rnC24MUpF+3XO/VKgFi+9PFqMyvQ2GHBBGb8H3cLSw7rDg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.EventLog": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "1C8eTuxF6BLncNSJ1HCfmaBcjpUSqQDPlBVdYTlet9oldHTPpNh9iatxSJLs8TOqdp/FOpH+nSLdBve7fu9mTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7",
|
||||||
|
"System.Diagnostics.EventLog": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.EventSource": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "YWfndnDX1jVMGCN8d5T+rO+BO8sDw6BkYlUk0BYui+WP7+HhlWx8QLdA4yUDjrkGVb3AQxIWWEPVKw5Nnfj5GQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "IT7f+EMXZtkjatEcF+o6aOw/7OE4etRrMiDGEWH/iiTu2R3uhC4NEQJCfHiibtX45U3sIQ5Fh6tbb1qaOz3YAg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Configuration.Binder": "10.0.7",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.7",
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Primitives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "D5M0Jr551iTgwkZMN9rm0pSkgNLj5quUWQUmQPMZh7k/bnvZTnXRGfE2KuvXf1EEjt/ofD9yw9IumpgdP9QCnw=="
|
||||||
|
},
|
||||||
|
"Microsoft.NET.StringTools": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "17.11.4",
|
||||||
|
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.11",
|
||||||
|
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
|
||||||
|
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.11",
|
||||||
|
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.11",
|
||||||
|
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
|
||||||
|
"dependencies": {
|
||||||
|
"SQLitePCLRaw.core": "2.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Diagnostics.EventLog": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.7",
|
||||||
|
"contentHash": "WbmDLeTPYhEzXhvYVioTVn/D1XX6bovyny9n5p8Zxtf03+eY385RB818teZm6n+fA63iZNvng0/Np4tLuhkMhQ=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||||
[](LICENSE)
|
[](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://github.com/goatcorp/Dalamud)
|
||||||
[](https://dotnet.microsoft.com/)
|
[](https://dotnet.microsoft.com/)
|
||||||
[](https://www.finalfantasyxiv.com/)
|
[](https://www.finalfantasyxiv.com/)
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Version 1.4.6** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
**Version 1.5.0** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
[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
|
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
|
||||||
@@ -286,20 +286,19 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
|
|||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
**Version 1.4.6** — Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop and
|
**Version 1.5.0** — DI Foundation and Service Refactor. Major architecture cycle: the plugin bootstrap moves to a
|
||||||
pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier reflow check and a markdownlint pass at
|
generic-host DI container (`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync. All
|
||||||
the pre-push gate. `FontManager`'s font-fallback catch-filter now covers `InvalidOperationException` and
|
18 instance-class services migrate from a static `Plugin.LogProxy` locator to `Microsoft.Extensions.Logging.ILogger<T>`
|
||||||
`ArgumentException` on top of the IO triad, so a corrupted font config no longer takes down the atlas build.
|
via constructor injection, with a custom `DalamudLogger` bridging the framework over to Dalamud's `IPluginLog`. The
|
||||||
`BrandingLinks` and `IntegrationLinks` URLs validate themselves on plugin load — a typo in a future URL rotation throws
|
proxy stays for the eight buckets ctor-injection cannot reach (static helpers like `EmoteCache`, Dalamud-reflected
|
||||||
at startup instead of failing silently when a user clicks the broken button. Cherry-picked from ChatTwo upstream
|
`Configuration`, the `Message` data class, and static methods inside `FontManager` / `GameFunctions`). Plugin.cs
|
||||||
`f35b7d3`: `Chat.SetChannel` no longer leaks the native `Utf8String` when the linkshell check rejects the channel, and
|
finishes the cycle at 1012 lines — virtually identical to the pre-cycle 1013 — because the new Phase-1 host build
|
||||||
`Tab.Clone` now deep-clones `UsedChannel` and `TellTarget` (the previous reference copy let PopOut and Temp tabs mutate
|
and Plugin.X bridge wiring trade out exactly the service and window allocations that left `LoadAsync`. Cross-plugin
|
||||||
each other's channel state). The active-tab underline pill scales with DPI and rounds to physical pixels for crisp
|
baseline confirms no performance penalty vs Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2 74 ms.
|
||||||
rendering above 100 % DPI. Internal items: `HellionStyle` ChildBgAlpha extracted to a testable helper,
|
Lightless and XIVInstantMessenger sit around 7 ms by deferring their font-atlas build past `Finished loading` —
|
||||||
`Plugin.SaveConfig` clones only the temp-tab subset, `SettingsOverview` caches the draw-list per frame,
|
that pattern is the v1.5.1 follow-up item. One user-visible fix bundled in from upstream: pasting a slash command
|
||||||
`Dalamud.Utility.Util` static surface routed through an `IPlatformUtil` indirection (`MessageStore`'s `IsWine` probe is
|
into the chat input (Friend List "/tell" action, plugin-driven inserts) now replaces the existing input instead of
|
||||||
now testable in isolation). No schema bump, no migration. Seventh sub-patch of the v1.4.x polish sweep series (as of
|
concatenating onto whatever the user was typing. Migration v17 stays (no schema bump).
|
||||||
2026-05-12).
|
|
||||||
|
|
||||||
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
||||||
|
|
||||||
|
|||||||
+175
-6
@@ -10,6 +10,175 @@ to the release pages for details.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Hellion Chat 1.5.0 — DI Foundation and Service Refactor (2026-05-17)
|
||||||
|
|
||||||
|
Major architecture cycle. The plugin bootstrap moves to a generic-host DI container
|
||||||
|
(`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync. Service
|
||||||
|
logging migrates from a static `Plugin.LogProxy` locator to typed
|
||||||
|
`Microsoft.Extensions.Logging.ILogger<T>` via constructor injection, bridged over Dalamud's
|
||||||
|
`IPluginLog` by a custom `DalamudLogger` trio.
|
||||||
|
|
||||||
|
### Under the hood
|
||||||
|
|
||||||
|
- 18 instance-class services migrate to `ILogger<T>` via constructor injection across four
|
||||||
|
slices: data layer (`MessageStore`, `MessageManager`, `AutoTellTabsService`), IPC and
|
||||||
|
integrations (`HonorificService`, `IpcManager`, `TypingIpc`, `ExtraChat`, three
|
||||||
|
`GameFunctions` classes), UI window layer (`ChatLogWindow`, `DbViewer`, `Popout`, three
|
||||||
|
settings tabs), and root (`Commands`, `ThemeRegistry`, `PayloadHandler`).
|
||||||
|
- `Plugin.LogProxy` stays in place for the eight buckets ctor injection cannot reach:
|
||||||
|
static helpers (`EmoteCache`, `AutoTranslate`, `MemoryUtil`, `WrapperUtil`),
|
||||||
|
Dalamud-reflected types (`Configuration`), the `Message` data class, and instance classes
|
||||||
|
that only log from static methods (`FontManager`, one `GameFunctions` site).
|
||||||
|
- Plugin.cs finishes at 1012 lines — virtually identical to the pre-cycle 1013. The new
|
||||||
|
Phase-1 host build and `Plugin.X` bridge wiring trade out exactly the service and window
|
||||||
|
allocations that previously lived in `LoadAsync`.
|
||||||
|
- Cross-plugin baseline confirms no performance penalty against Chat 2: HellionChat
|
||||||
|
first-frame HITCH 77 ms median, Chat 2 74 ms median. Lightless and XIVInstantMessenger sit
|
||||||
|
around 7 ms by deferring their font-atlas build past `Finished loading` — that pattern is
|
||||||
|
the v1.5.1 follow-up item.
|
||||||
|
|
||||||
|
### User-visible
|
||||||
|
|
||||||
|
- Slash-command insert fix: pasting a slash command into the chat input (Friend List
|
||||||
|
"/tell" action, plugin-driven inserts from Artisan, AllaganTools etc.) now replaces the
|
||||||
|
existing input instead of concatenating onto whatever the user was typing. Cherry-picked
|
||||||
|
from ChatTwo upstream `ee7768ac` with namespace adaptation.
|
||||||
|
|
||||||
|
Migration v17 stays (no schema bump).
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
that survive relog, opt-in Honorific glow rendering, a configurable sidebar, plus a Settings-Save channel-preservation
|
||||||
|
fix surfaced during smoke testing.
|
||||||
|
|
||||||
|
- TempTell Pin: right-click a TempTell tab in the sidebar and choose "Pin Tab" / "Tab anpinnen". Pinned tabs survive
|
||||||
|
plugin reload and character logout, keep their conversation history (loaded on demand from the message store on
|
||||||
|
rehydrate), and stay bound to the same `/tell` partner. Hard cap of 5 pinned tabs in a pool separate from the 15-tab
|
||||||
|
auto-tell pool — total ceiling is 20 tabs. The sidebar groups pinned tabs into their own section with a divider header
|
||||||
|
- Honorific glow outlines now render via an 8-direction DrawList pre-pass when the title carries a Glow colour. Opt-in
|
||||||
|
via **Settings → Integrations → Render glow outlines (Honorific)** (default off). Honorific's gradient surface
|
||||||
|
(`Color3`, `GradientColourSet`, `GradientAnimationStyle`) is parsed and stashed for a later cycle but renders as the
|
||||||
|
primary colour until then — the v1.4.7 DTO already mirrors all four extra fields so the JSON roundtrip doesn't
|
||||||
|
silent-drop them
|
||||||
|
- Sidebar width configurable in **Theme & Layout** (44–160 px, default 44 stays icon-only). The icon button stretches
|
||||||
|
with the configured width so a widened sidebar looks intentional, not a 36 px icon floating in empty space
|
||||||
|
- `Configuration.UpdateFrom` now preserves the runtime `CurrentChannel` across the persistent-tab merge alongside
|
||||||
|
`Messages` and `LastSendUnread`. `TabSwitched` deep-clones the seeded channel from the previous tab instead of sharing
|
||||||
|
the same `UsedChannel` instance. Together these fix a regression where Settings-Save on a Party or Linkshell tab
|
||||||
|
popped the chat input back to `/tell <pinned-partner>` on the next interaction
|
||||||
|
- `Util/ImGuiUtil.cs` `DrawArrows` IconButton id uses `(id + 1).ToString()` with explicit parentheses instead of the
|
||||||
|
operator-precedence quirk `id + 1.ToString()` (which resolved to `id.ToString() + "1"`). Single live caller is
|
||||||
|
`Ui/DbViewer.cs:227` page-navigation
|
||||||
|
- Internal: `IPluginLogProxy` indirection over Dalamud's `IPluginLog` routes all ~91 `Plugin.Log` call sites through a
|
||||||
|
testable proxy. `MessageStore.Migrate0` can now run in xUnit without loading `Dalamud.dll`, closing the gap F12.1 left
|
||||||
|
in v1.4.6. Production wrapper `DalamudPluginLogProxy` and Build-Suite `FakePluginLogProxy` mirror the full
|
||||||
|
`IPluginLog` surface (`Verbose`/`Debug`/`Information`/`Info`/`Warning`/`Error`/`Fatal`) with single-string,
|
||||||
|
`Exception+string`, and `params object[]` overloads
|
||||||
|
- Internal: TempTab counter switched from an `Interlocked` cached field to a derived `Tabs.Count(predicate)`. Pin-state
|
||||||
|
transitions (TryPin / Unpin / Promote) are cold-path and don't need lock-free reads; counter mutation surface dropped
|
||||||
|
from 5 to 0 sites. Build-Suite floor 688 → 710 (+22)
|
||||||
|
- Schema bump v16 → v17 is additive: new `Tab.IsPinned` bool, default false. Existing v16 configs load cleanly and get
|
||||||
|
their `Version` stamp bumped after the gate check
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Hellion Chat 1.4.6 — Code Hygiene and Refactor (2026-05-12)
|
## Hellion Chat 1.4.6 — Code Hygiene and Refactor (2026-05-12)
|
||||||
|
|
||||||
Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two
|
Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two
|
||||||
@@ -20,8 +189,8 @@ upstream-inherited bugs from ChatTwo `f35b7d3`, and prepares the code for the v1
|
|||||||
`MD036` so the bilingual forge-post bold-emphasis headings pass linting; the `.claude/` directory is excluded from the
|
`MD036` so the bilingual forge-post bold-emphasis headings pass linting; the `.claude/` directory is excluded from the
|
||||||
scan
|
scan
|
||||||
- `FontManager.AddFontWithFallback` catch-filter now covers `InvalidOperationException` and `ArgumentException` on top
|
- `FontManager.AddFontWithFallback` catch-filter now covers `InvalidOperationException` and `ArgumentException` on top
|
||||||
of the existing IO triad. The warning log carries the exception type name, so the diagnostic path knows which class
|
of the existing IO triad. The warning log carries the exception type name, so the diagnostic path knows which class of
|
||||||
of atlas-toolkit throw triggered the NotoSansCjkRegular fallback
|
atlas-toolkit throw triggered the NotoSansCjkRegular fallback
|
||||||
- `BrandingLinks` (5 URLs) and `Integrations/IntegrationLinks` (2 URLs) validate themselves on first module load via
|
- `BrandingLinks` (5 URLs) and `Integrations/IntegrationLinks` (2 URLs) validate themselves on first module load via
|
||||||
`[ModuleInitializer]` + a shared `UrlValidation.ValidateAll` helper. A malformed URL now throws
|
`[ModuleInitializer]` + a shared `UrlValidation.ValidateAll` helper. A malformed URL now throws
|
||||||
`InvalidOperationException` at plugin load with the source class and the broken URL in the message
|
`InvalidOperationException` at plugin load with the source class and the broken URL in the message
|
||||||
@@ -47,10 +216,10 @@ upstream-inherited bugs from ChatTwo `f35b7d3`, and prepares the code for the v1
|
|||||||
indirection. `MessageStore`'s `IsWine` probe is now reachable from the xUnit AppDomain via a `FakePlatformUtil`
|
indirection. `MessageStore`'s `IsWine` probe is now reachable from the xUnit AppDomain via a `FakePlatformUtil`
|
||||||
fixture (full isolated MessageStore construction still pending — `Plugin.Log.Information` in `Migrate0` is a separate
|
fixture (full isolated MessageStore construction still pending — `Plugin.Log.Information` in `Migrate0` is a separate
|
||||||
Dalamud-static surface, slated for v1.4.7)
|
Dalamud-static surface, slated for v1.4.7)
|
||||||
- Built-in themes: Crystal Nocturne (royal sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces
|
- Built-in themes: Crystal Nocturne (royal sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit
|
||||||
Moonlit Bloom in the built-in roster. Users who had Moonlit Bloom selected fall back to the default Hellion Arctic
|
Bloom in the built-in roster. Users who had Moonlit Bloom selected fall back to the default Hellion Arctic on the
|
||||||
on the first plugin load; an existing custom JSON copy of Moonlit Bloom under `pluginConfigs/HellionChat/themes/`
|
first plugin load; an existing custom JSON copy of Moonlit Bloom under `pluginConfigs/HellionChat/themes/` keeps
|
||||||
keeps working unchanged
|
working unchanged
|
||||||
|
|
||||||
Modding & support: join Hellion Forge — <https://discord.gg/X9V7Kcv5gR>
|
Modding & support: join Hellion Forge — <https://discord.gg/X9V7Kcv5gR>
|
||||||
|
|
||||||
|
|||||||
+120
-20
@@ -10,32 +10,132 @@ the plugin's privacy-first scope during brainstorming.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Next Cycle (v1.4.7)
|
## Next Cycle (v1.5.1)
|
||||||
|
|
||||||
**Backlog Cleanup.** Roll up the remaining audit items deferred from v1.4.0–v1.4.6 and the new entries surfaced during
|
**Honorific Full Gradient Port plus FontAtlas-Defer for a 10× HITCH cut.** v1.5.0 closed the DI-container cycle with
|
||||||
v1.4.6 (notably the `Plugin.Log` indirection that would unlock fully isolated `MessageStore` construction tests, plus
|
no performance penalty against Chat 2 (77 ms vs 74 ms median first-frame HITCH), but the cross-plugin baseline against
|
||||||
follow-up scope hinted at in the ChatTwo upstream f35b7d3 cherry-picks). Scope is consolidated during brainstorm.
|
Lightless Sync and XIVInstantMessenger surfaced a clean optimisation: both plugins defer their font-atlas build until
|
||||||
|
after `Finished loading` and sit at 6-7 ms HITCH, an order of magnitude below the ~75 ms floor that Chat 2 and HellionChat
|
||||||
|
share. v1.5.1 ports that pattern. Plus the Honorific gradient render path — DTO is gradient-ready since v1.4.7, only the
|
||||||
|
Wave / Pulse animation port remains. After that, First-Run-Wizard rework with curated defaults beyond the three privacy
|
||||||
|
profiles, then FR localisation (Hezcal native-speaker review confirmed), then the Plugin Integrations Wave 2-6
|
||||||
|
(Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM). Wine/Linux scroll-rubber-band spike sits as a
|
||||||
|
low-priority Linux-only investigation at the tail.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.5.0 — DI Foundation and Service Refactor (released 2026-05-17)
|
||||||
|
|
||||||
|
Major architecture cycle. Plugin bootstrap moves to a generic-host DI container
|
||||||
|
(`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync's `PluginHostFactory`. Service
|
||||||
|
logging migrates from the static `Plugin.LogProxy` locator (the F12.2 shim from v1.4.7) to typed
|
||||||
|
`Microsoft.Extensions.Logging.ILogger<T>` via constructor injection, bridged over Dalamud's `IPluginLog` by a custom
|
||||||
|
`DalamudLogger` trio. 18 instance-class services move to ctor-injected loggers across four slices: data layer,
|
||||||
|
IPC/integrations, UI window layer, and root. `Plugin.LogProxy` stays for the eight buckets ctor injection cannot
|
||||||
|
reach — static helpers (`EmoteCache`, `AutoTranslate`, `MemoryUtil`, `WrapperUtil`), Dalamud-reflected types
|
||||||
|
(`Configuration`), the `Message` data class, and instance classes that only log from static methods (`FontManager`,
|
||||||
|
one `GameFunctions` site). Plugin.cs finishes at 1012 lines, virtually identical to the pre-cycle 1013 (-1 netto): the
|
||||||
|
new Phase-1 host build and `Plugin.X` bridge wiring trade out exactly the service and window allocations that previously
|
||||||
|
lived in `LoadAsync`. Cross-plugin baseline (10 reload-stress runs, 51 active plugins): HellionChat first-frame HITCH
|
||||||
|
77 ms median, Chat 2 v1.40.2 74 ms median — no DI penalty. The deferred-font-atlas pattern from Lightless and
|
||||||
|
XIVInstantMessenger is the v1.5.1 follow-up. User-visible: slash-command insert fix cherry-picked from ChatTwo upstream
|
||||||
|
`ee7768ac` — pasting a slash command into the chat input now replaces existing input instead of concatenating.
|
||||||
|
Migration v17 stays.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.7 — Backlog Cleanup and Mid-Features (released 2026-05-13)
|
||||||
|
|
||||||
|
Eighth sub-patch of the v1.4.x Polish Sweep series. First user-visible feature bundle since v1.4.5. TempTell tabs can
|
||||||
|
now be pinned via right-click; pinned tabs survive plugin reload and character logout, keep their conversation history
|
||||||
|
(loaded on demand from the message store on rehydrate), 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, total ceiling 20. The sidebar groups pinned tabs
|
||||||
|
into their own section with a divider header, and the sidebar width itself is now configurable in **Theme & Layout**
|
||||||
|
between 44 and 160 px. Honorific glow outlines render when the title carries a Glow colour, opt-in via **Settings →
|
||||||
|
Integrations → Render glow outlines (Honorific)** (default off). Honorific's gradient (Color3 / GradientColourSet / Wave
|
||||||
|
/ Pulse) is parsed but rendered statically — a later cycle will port the full animation algorithm or land an upstream
|
||||||
|
IPC PR for the resolved frame colour. `Configuration.UpdateFrom` now preserves the runtime `CurrentChannel` across the
|
||||||
|
persistent-tab merge, and `TabSwitched` deep-clones the seeded channel instead of sharing the previous tab's
|
||||||
|
`UsedChannel` — together they fix a Settings-Save regression where the chat input could pop back to
|
||||||
|
`/tell <pinned-partner>` after touching settings 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
|
||||||
|
F12.1 test-isolation gap (`MessageStore.Migrate0` runs in xUnit now). TempTab counter switched from `Interlocked` cached
|
||||||
|
field to derived `Tabs.Count(predicate)`. Migration v16 → v17 is additive (new `Tab.IsPinned` flag). Build-Suite floor
|
||||||
|
688 → 710 (+22 tests across Pin-lifecycle predicates, pool limits, Tab.Clone roundtrip, MessageStore Migrate0
|
||||||
|
construction, and Honorific TitleData JSON roundtrip).
|
||||||
|
|
||||||
## v1.4.6 — Code Hygiene and Refactor (released 2026-05-12)
|
## v1.4.6 — Code Hygiene and Refactor (released 2026-05-12)
|
||||||
|
|
||||||
Seventh sub-patch of the v1.4.x Polish Sweep series. Maintenance patch — no user-visible behaviour changes; tightens
|
Seventh sub-patch of the v1.4.x Polish Sweep series. Maintenance patch — no user-visible behaviour changes; tightens the
|
||||||
the development feedback loop and pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier
|
development feedback loop and pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier reflow
|
||||||
reflow check (Block E) and a markdownlint pass (Block F), so style drift and markdown violations are blocked at the
|
check (Block E) and a markdownlint pass (Block F), so style drift and markdown violations are blocked at the pre-push
|
||||||
pre-push gate. `FontManager.AddFontWithFallback` catch-filter now spans `InvalidOperationException` and
|
gate. `FontManager.AddFontWithFallback` catch-filter now spans `InvalidOperationException` and `ArgumentException` on
|
||||||
`ArgumentException` on top of the existing IO triad, with the exception type name in the warning log so the
|
top of the existing IO triad, with the exception type name in the warning log so the diagnostic path can see which
|
||||||
diagnostic path can see which atlas-toolkit throw triggered the fallback. `BrandingLinks` and `IntegrationLinks` run a
|
atlas-toolkit throw triggered the fallback. `BrandingLinks` and `IntegrationLinks` run a `[ModuleInitializer]` URL
|
||||||
`[ModuleInitializer]` URL validation pass on plugin load; a typo in a future URL rotation now throws at startup
|
validation pass on plugin load; a typo in a future URL rotation now throws at startup instead of failing silently when a
|
||||||
instead of failing silently when a user clicks the broken button. Cherry-picked from ChatTwo upstream `f35b7d3`:
|
user clicks the broken button. Cherry-picked from ChatTwo upstream `f35b7d3`: `Chat.SetChannel` no longer leaks the
|
||||||
`Chat.SetChannel` no longer leaks the native `Utf8String` when the linkshell check rejects the channel (rename to
|
native `Utf8String` when the linkshell check rejects the channel (rename to `IsChannelOrExistingLinkshell` plus
|
||||||
`IsChannelOrExistingLinkshell` plus wrap-not-return), and `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget`
|
wrap-not-return), and `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget` (the previous reference copy let PopOut
|
||||||
(the previous reference copy let PopOut and Temp tabs mutate each other's channel state). The `ChatLogWindow`
|
and Temp tabs mutate each other's channel state). The `ChatLogWindow` active-tab underline pill scales with
|
||||||
active-tab underline pill scales with `ImGuiHelpers.GlobalScale` and rounds to physical pixels for crisp rendering
|
`ImGuiHelpers.GlobalScale` and rounds to physical pixels for crisp rendering above 100 % DPI. Internal items:
|
||||||
above 100 % DPI. Internal items: `HellionStyle` ChildBgAlpha extracted to a testable helper, `Plugin.SaveConfig`
|
`HellionStyle` ChildBgAlpha extracted to a testable helper, `Plugin.SaveConfig` clones only the temp-tab subset in the
|
||||||
clones only the temp-tab subset in the snapshot path, `SettingsOverview` caches the draw-list per frame,
|
snapshot path, `SettingsOverview` caches the draw-list per frame, `Dalamud.Utility.Util` static surface routed through
|
||||||
`Dalamud.Utility.Util` static surface routed through an `IPlatformUtil` indirection (`MessageStore`'s `IsWine` probe
|
an `IPlatformUtil` indirection (`MessageStore`'s `IsWine` probe is now testable in isolation). No schema bump, no
|
||||||
is now testable in isolation). No schema bump, no migration.
|
migration.
|
||||||
|
|
||||||
## v1.4.5 — UX and Robustness (released 2026-05-12)
|
## v1.4.5 — UX and Robustness (released 2026-05-12)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ TAG="v$VER"
|
|||||||
grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" "$YAML" \
|
grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" "$YAML" \
|
||||||
|| fail "$YAML changelog missing **v${VER}** subblock. Fix: add the v${VER} block at the top of the changelog field."
|
|| fail "$YAML changelog missing **v${VER}** subblock. Fix: add the v${VER} block at the top of the changelog field."
|
||||||
|
|
||||||
jq -r '.[0].Changelog' "$REPO_JSON" | grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" \
|
# Process substitution instead of `jq | grep -q` — grep -q closes stdin on the
|
||||||
|
# first match, jq keeps writing the multi-KB Changelog string and trips SIGPIPE,
|
||||||
|
# which `set -o pipefail` then turns into a false-positive FAIL. Manifested as a
|
||||||
|
# `jq: writing output failed: Broken pipe` line plus a misleading "Changelog
|
||||||
|
# missing **vX.Y.Z** subblock" message during pre-push runs.
|
||||||
|
grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" <(jq -r '.[0].Changelog' "$REPO_JSON") \
|
||||||
|| fail "$REPO_JSON Changelog missing **v${VER}** subblock. Fix: copy the yaml changelog over."
|
|| fail "$REPO_JSON Changelog missing **v${VER}** subblock. Fix: copy the yaml changelog over."
|
||||||
|
|
||||||
FORGE_FILE="$FORGE_DIR/${TAG}.md"
|
FORGE_FILE="$FORGE_DIR/${TAG}.md"
|
||||||
|
|||||||
Reference in New Issue
Block a user