Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3fdcdf43d | |||
| f4ea460644 | |||
| d5735d8dcc | |||
| 80b48ac3ad | |||
| cddd29a986 | |||
| 799fdb67cc | |||
| 69fa0fecbd | |||
| fd5f970a8b | |||
| fee2459e73 | |||
| 63cad62c89 | |||
| dca5de4085 | |||
| 8edc3c70d3 | |||
| 3c33acf6d7 | |||
| c8ba8c1cd0 | |||
| 94e4828aeb | |||
| 1d88cb4c42 | |||
| c5fe69f0d3 | |||
| b46d3ad0a8 | |||
| e33cf0dcb9 | |||
| 0d016aaa5d | |||
| 5b972238bb | |||
| 7ac1eb3fd4 | |||
| db48f27842 | |||
| f8b5c14509 | |||
| 28e4b30cd6 | |||
| 4510c1e404 | |||
| 6b44f549b4 | |||
| ae1436b103 | |||
| 2684c31f10 | |||
| bdd64cad07 | |||
| 28ea2fa553 | |||
| dd597fca44 | |||
| b9d3ff8f26 | |||
| df3d5d78d6 | |||
| 2e057ce6c4 | |||
| e5dbc333fa | |||
| d0ec94c3e6 | |||
| cafb6faa39 | |||
| b8d289a847 | |||
| f16d8f5c78 | |||
| eabb39ba86 | |||
| b489ac946c | |||
| 8d9151c74a | |||
| 4ecbaf2a4b | |||
| 3e4601a0c8 |
@@ -120,17 +120,37 @@ jobs:
|
||||
$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"
|
||||
$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"
|
||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
||||
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"
|
||||
$releaseUrl = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
|
||||
# ---------- 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]@{
|
||||
username = "Forge Herald"
|
||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||
@@ -142,9 +162,14 @@ jobs:
|
||||
embeds = @(
|
||||
[ordered]@{
|
||||
title = $title
|
||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
url = $releaseUrl
|
||||
color = 12730636
|
||||
description = $description
|
||||
description = $deDesc
|
||||
},
|
||||
[ordered]@{
|
||||
url = $releaseUrl
|
||||
color = 12730636
|
||||
description = $enDesc
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
}
|
||||
|
||||
@@ -17,19 +17,18 @@ spricht jetzt bei unbekannten ChatTypes.
|
||||
IPC-Callback-Methode jetzt einen 1-Zeilen-Banner darüber, der den Thread-Kontext direkt am Call-Site benennt
|
||||
(framework only, framework scheduled, any). Mehr Hilfe für künftige Reviews als ein abstraktes Threading-Kapitel
|
||||
- **Unsubscribe-Failure ist jetzt sichtbar.** `TryUnsubscribe` hat ein Honorific-Unsubscribe-Failure bisher als Debug
|
||||
geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription kann den Service über
|
||||
Plugin-Reloads hinweg leben lassen, also läuft der Log jetzt auf Warning
|
||||
geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription kann den Service über Plugin-Reloads
|
||||
hinweg leben lassen, also läuft der Log jetzt auf Warning
|
||||
- **AutoTranslate-Warmup blockiert den Plugin-Unload nicht mehr.** Der Cache-Warmup-Thread war ohne `IsBackground=true`
|
||||
unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu MessageManager und RetentionSweep
|
||||
(beide seit v1.4.0)
|
||||
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType einführt der weder
|
||||
in der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der
|
||||
Filter einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet,
|
||||
also kein Log-Spam
|
||||
unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu MessageManager und RetentionSweep (beide
|
||||
seit v1.4.0)
|
||||
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType einführt der weder in
|
||||
der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der Filter
|
||||
einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet, also kein
|
||||
Log-Spam
|
||||
- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen Configs jetzt auf `true`,
|
||||
damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt wird bevor der User entscheiden kann.
|
||||
Bestehende Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein
|
||||
Schema-Bump
|
||||
damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt wird bevor der User entscheiden kann. Bestehende
|
||||
Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein Schema-Bump
|
||||
|
||||
Keine User-sichtbaren Funktions-Änderungen außer dem Default-Flip für neue Installationen. Settings, Themes, Tabs und
|
||||
das Privacy-Verhalten für Bestand bleiben unangetastet.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
subtitle: UX und Robustheit
|
||||
versionsnatur: UX-Polish-Cycle
|
||||
---
|
||||
|
||||
**Hellion Chat 1.4.5 — UX und Robustheit**
|
||||
|
||||
Sechster Sub-Patch der v1.4.x Polish-Sweep-Serie. Render-Fehler im Chat-Fenster werden jetzt sichtbar, der
|
||||
First-Run-Wizard hat eine explizite Cancel-Schaltfläche, der Eingabe-Verlauf bleibt nicht mehr über Plugin-Reloads
|
||||
hinweg liegen, und die Statusleiste klippt in schmalen Fenstern nicht mehr.
|
||||
|
||||
- **Fehler-Benachrichtigung im Chat-Fenster.** Wenn ein Render-Fehler in `DrawChatLog` auftritt, zeigt das Plugin jetzt
|
||||
eine einmalige Warning-Notification mit Verweis aufs `/xllog`, statt das Fenster stillschweigend leer zu lassen. Der
|
||||
Stack-Trace selbst geht weiter via `Plugin.Log.Error` ins Logfile. De-Dup über Per-Session-Bool, damit ein
|
||||
wiederkehrender Fehler die Notification-Stack nicht pro Frame neu vollkippt
|
||||
- **First-Run-Wizard trennt Accept und Close.** `OnClose` setzt nicht mehr stillschweigend `FirstRunCompleted=true`,
|
||||
also lässt das X den Wizard schwebend zurück und er kommt beim nächsten Plugin-Reload wieder. Eine neue „Später —
|
||||
Defaults behalten"-Schaltfläche im Footer ist der explizite Weg, ohne Profil-Auswahl rauszukommen. Strings bilingual
|
||||
EN+DE plus Tooltip
|
||||
- **Eingabe-Verlauf wird beim Plugin-Reload geleert.** `InputHistoryService.Reset` hängt jetzt in `Plugin.DisposeAsync`
|
||||
neben den anderen Pure-Memory-Cleanups, damit der statische Zustand aus der vorigen Session den nächsten Load nicht
|
||||
mehr erbt
|
||||
- **Statusleiste klippt nicht mehr.** Der rechtsbündige Versions-Slot wird ausgeblendet wenn die Chat-Window-Breite
|
||||
abzüglich Versions-Text unter 200 px fällt — vorher überlappte er die vier linken Slots. Ab ausreichender Breite
|
||||
taucht der Slot wieder auf
|
||||
- **Intern:** `FontManager` fällt auf System-Font zurück wenn die eingebettete Hellion-Font-Resource fehlt
|
||||
(Broken-csproj-Pfad, nie ein Produktions-Build), plus expliziter Session-Only-Invariant-Kommentar für Auto-Tell-Tabs
|
||||
in `Plugin.cs:167-168` mit einem TempTabCounter-Init-Pin in der Build-Suite. Kein Schema-Bump, keine Migration
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
subtitle: Code Hygiene and Refactor
|
||||
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`.
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
- **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.
|
||||
- **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).
|
||||
@@ -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).
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"MD007": { "indent": 4 },
|
||||
"MD013": false,
|
||||
"MD024": { "siblings_only": true },
|
||||
"MD029": false,
|
||||
"MD033": false,
|
||||
"MD036": false,
|
||||
"MD041": false
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
@@ -20,13 +21,11 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
private readonly MessageStore _store;
|
||||
private readonly object _tempTabsLock = new();
|
||||
|
||||
// F2.1: lock-free counter mirrors Config.Tabs.Count(IsTempTab) so the
|
||||
// hot-path getter doesn't contend with HandleTell on every render frame.
|
||||
// Bumped from inside the existing mutation paths so it stays consistent
|
||||
// with the underlying list — see SpawnTempTab, DropOldestTempTab, OnLogout
|
||||
// and ResyncTempTabCounter (used by Plugin.cs snapshot-restore).
|
||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
||||
private int _activeTempTabCount;
|
||||
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
|
||||
// of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
|
||||
// in their own bucket. A configurable cap is a vault-backlog anchor for
|
||||
// a later cycle if tester feedback demands it.
|
||||
internal const int MaxPinnedTempTabs = 5;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
@@ -37,7 +36,14 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
_store = store;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -46,23 +52,51 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Seed the counter from the persisted Tabs list so a config that already
|
||||
// contains TempTabs from a prior session starts in sync. Plugin.cs:168
|
||||
// crash-recovery has already dropped TempTabs by the time we get here,
|
||||
// so the snapshot reflects post-recovery reality.
|
||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
||||
// Pinned tabs come out of the JSON with TellTarget set but
|
||||
// CurrentChannel reset (NonSerialized). Without re-seeding, the chat
|
||||
// input has no tell-target on the active pinned tab, and the
|
||||
// game-side channel hook only repaints CurrentChannel once the user
|
||||
// triggers a /tell or channel switch.
|
||||
RehydratePinnedTabs();
|
||||
|
||||
_messageManager.MessageProcessed += HandleTell;
|
||||
Plugin.ClientState.Logout += OnLogout;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
// F2.1: callable from outside paths that mutate Config.Tabs directly
|
||||
// (Plugin.cs snapshot-restore). Atomically re-pegs the counter to the
|
||||
// live IsTempTab count.
|
||||
internal void ResyncTempTabCounter()
|
||||
private void RehydratePinnedTabs()
|
||||
{
|
||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
||||
var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||
Plugin.LogProxy.Debug($"[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())
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
$"[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);
|
||||
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -96,7 +130,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
if (partner == null)
|
||||
{
|
||||
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
||||
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
||||
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
||||
@@ -110,7 +144,23 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -160,22 +210,35 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
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.TellTarget != null
|
||||
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
||||
&& 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
|
||||
.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)
|
||||
.ThenBy(t => t.Tab.LastActivity)
|
||||
.FirstOrDefault();
|
||||
@@ -198,7 +261,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||
Interlocked.Decrement(ref _activeTempTabCount);
|
||||
|
||||
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||
if (victim.Index <= _plugin.LastTab)
|
||||
@@ -223,7 +285,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.Add(tab);
|
||||
Interlocked.Increment(ref _activeTempTabCount);
|
||||
}
|
||||
|
||||
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
||||
@@ -300,7 +361,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
||||
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
|
||||
Plugin.LogProxy.Error(ex, "[AutoTellTabs] History preload failed");
|
||||
tab.Messages.AddPrune(
|
||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||
MessageManager.MessageDisplayLimit
|
||||
@@ -354,14 +415,16 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
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 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
|
||||
.Config.Tabs.Where(t => t.IsTempTab && t.PopOut)
|
||||
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
|
||||
.Select(t => t.Identifier)
|
||||
.ToList();
|
||||
if (poppedTempTabIds.Count > 0)
|
||||
@@ -377,15 +440,78 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
var removed = Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
Interlocked.Add(ref _activeTempTabCount, -removed);
|
||||
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
// 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;
|
||||
if (currentWasTempTab || !stillValid)
|
||||
if (currentWasUnpinnedTempTab || !stillValid)
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryPin(Tab tab)
|
||||
{
|
||||
if (!tab.IsTempTab || tab.IsPinned)
|
||||
{
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[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;
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[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;
|
||||
Plugin.LogProxy.Debug("[Pin] Unpinned tab '{tab.Name}'");
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
internal void PromoteToPermanent(Tab tab)
|
||||
{
|
||||
if (!tab.IsTempTab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tab.IsTempTab = false;
|
||||
tab.IsPinned = false;
|
||||
tab.TellTarget = TellTarget.Empty();
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)"
|
||||
);
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Branding;
|
||||
|
||||
// Centralised — a future invite/URL rotation only touches this file.
|
||||
@@ -9,4 +12,22 @@ internal static class BrandingLinks
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
||||
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
||||
public const string HellionMediaWebsite = "https://hellion-media.de/de";
|
||||
|
||||
// CA2255 warns against [ModuleInitializer] in library code, but Dalamud
|
||||
// loads the plugin DLL directly so the module-init pass is the right hook
|
||||
// for a one-shot URL sanity check at plugin load.
|
||||
#pragma warning disable CA2255
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255
|
||||
internal static void ValidateUrls()
|
||||
{
|
||||
UrlValidation.ValidateAll(
|
||||
nameof(BrandingLinks),
|
||||
HellionForgeDiscordInvite,
|
||||
HellionForgeGitea,
|
||||
HellionChatRepo,
|
||||
HellionForgeWebsite,
|
||||
HellionMediaWebsite
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ internal sealed class Commands : IDisposable
|
||||
{
|
||||
if (!Registered.TryGetValue(command, out var wrapper))
|
||||
{
|
||||
Plugin.Log.Warning($"Missing registration for command {command}");
|
||||
Plugin.LogProxy.Warning($"Missing registration for command {command}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ internal sealed class Commands : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, $"Error while executing command {command}");
|
||||
Plugin.LogProxy.Error(ex, $"Error while executing command {command}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private const int LatestVersion = 16;
|
||||
private const int LatestVersion = 17;
|
||||
|
||||
public int Version { get; set; } = LatestVersion;
|
||||
|
||||
@@ -83,7 +83,7 @@ public class Configuration : IPluginConfiguration
|
||||
// silently, like before.
|
||||
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
||||
type,
|
||||
PrivacyPersistUnknownChannels
|
||||
@@ -102,10 +102,22 @@ public class Configuration : IPluginConfiguration
|
||||
public bool FirstRunCompleted;
|
||||
public bool UseHellionFont = 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 int AutoTellTabsLimit = 15;
|
||||
public bool AutoTellTabsCompactDisplay;
|
||||
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 SeenPopOutInputHint;
|
||||
public bool PopOutInputEnabled = true;
|
||||
@@ -278,16 +290,20 @@ public class Configuration : IPluginConfiguration
|
||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||
|
||||
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||
// not destroy open tell conversations. For persistent tabs, capture
|
||||
// the live MessageList and LastSendUnread by Identifier before the
|
||||
// replace and restore them onto the freshly cloned tabs; new tabs
|
||||
// get an empty MessageList, deleted tabs lose their history (intended).
|
||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
||||
var livePersistentSession = Tabs.Where(t => !t.IsTempTab)
|
||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
|
||||
// not destroy open tell conversations. Pinned TempTabs are persistent
|
||||
// and come through `other` like regular tabs; unpinned TempTabs are
|
||||
// session-only and held from the local state. For persistent tabs
|
||||
// (incl. pinned), capture live runtime state by Identifier and restore
|
||||
// it onto the freshly cloned tabs — CurrentChannel is critical because
|
||||
// the user may have switched channel in-game between settings-open
|
||||
// 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.Where(t => !t.IsTempTab)
|
||||
.Tabs.Where(t => !t.IsTempTab || t.IsPinned)
|
||||
.Select(t =>
|
||||
{
|
||||
var clone = t.Clone();
|
||||
@@ -295,11 +311,12 @@ public class Configuration : IPluginConfiguration
|
||||
{
|
||||
clone.Messages = live.Messages;
|
||||
clone.LastSendUnread = live.LastSendUnread;
|
||||
clone.CurrentChannel = live.CurrentChannel;
|
||||
}
|
||||
return clone;
|
||||
})
|
||||
.ToList();
|
||||
Tabs.AddRange(liveTempTabs);
|
||||
Tabs.AddRange(liveUnpinnedTempTabs);
|
||||
|
||||
ChatTabForward = other.ChatTabForward;
|
||||
ChatTabBackward = other.ChatTabBackward;
|
||||
@@ -319,6 +336,7 @@ public class Configuration : IPluginConfiguration
|
||||
FirstRunCompleted = other.FirstRunCompleted;
|
||||
UseHellionFont = other.UseHellionFont;
|
||||
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
||||
ShowHonorificGlow = other.ShowHonorificGlow;
|
||||
|
||||
// v1.1.0 theme engine fields
|
||||
Theme = other.Theme;
|
||||
@@ -330,6 +348,7 @@ public class Configuration : IPluginConfiguration
|
||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||||
SidebarWidth = other.SidebarWidth;
|
||||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||||
|
||||
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||||
@@ -404,6 +423,11 @@ public class Tab
|
||||
public bool HideWhenInactive;
|
||||
|
||||
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 TellTarget TellTarget = TellTarget.Empty();
|
||||
|
||||
@@ -500,7 +524,7 @@ public class Tab
|
||||
Opacity = Opacity,
|
||||
Identifier = Identifier,
|
||||
InputDisabled = InputDisabled,
|
||||
CurrentChannel = CurrentChannel,
|
||||
CurrentChannel = CurrentChannel.Clone(),
|
||||
CanMove = CanMove,
|
||||
CanResize = CanResize,
|
||||
IndependentHide = IndependentHide,
|
||||
@@ -511,8 +535,9 @@ public class Tab
|
||||
HideInBattle = HideInBattle,
|
||||
HideWhenInactive = HideWhenInactive,
|
||||
IsTempTab = IsTempTab,
|
||||
IsPinned = IsPinned,
|
||||
AllSenderMessages = AllSenderMessages,
|
||||
TellTarget = TellTarget.From(TellTarget),
|
||||
TellTarget = TellTarget.Clone(),
|
||||
IsGreeted = IsGreeted,
|
||||
};
|
||||
}
|
||||
@@ -690,6 +715,29 @@ public class UsedChannel
|
||||
{
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Deep-clone the UsedChannel so Tab.Clone() no longer shares
|
||||
// channel state (incl. TellTarget) with its origin Tab. Previously
|
||||
// a reference copy: PopOut and Temp tabs mutated each other.
|
||||
// - Name is intentionally a reference copy (matches upstream); it
|
||||
// gets reassigned on every channel switch anyway.
|
||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/UsedChannelCloneTests.cs
|
||||
// ---------------------------------------------------------------
|
||||
public UsedChannel Clone()
|
||||
{
|
||||
return new UsedChannel
|
||||
{
|
||||
Channel = Channel,
|
||||
Name = Name,
|
||||
TellTarget = TellTarget?.Clone(),
|
||||
|
||||
UseTempChannel = UseTempChannel,
|
||||
TempChannel = TempChannel,
|
||||
TempTellTarget = TempTellTarget?.Clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -101,7 +101,10 @@ public static class EmoteCache
|
||||
t =>
|
||||
{
|
||||
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
|
||||
)
|
||||
@@ -158,7 +161,7 @@ public static class EmoteCache
|
||||
{
|
||||
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
||||
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
|
||||
{
|
||||
Plugin.Log.Error("Failed to convert");
|
||||
Plugin.LogProxy.Error("Failed to convert");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -304,7 +307,7 @@ public static class EmoteCache
|
||||
catch (Exception ex)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-10
@@ -44,16 +44,26 @@ public class FontManager
|
||||
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
||||
private static byte[]? HellionFontBytes;
|
||||
|
||||
private static byte[] GetHellionFontBytes()
|
||||
// Returns null when the embedded font resource is missing. Should never
|
||||
// happen on a signed release build, but a broken csproj or hand-rolled
|
||||
// dev build can land here. Caller falls back to the system font path so
|
||||
// the plugin still loads instead of crashing the whole UiBuilder.
|
||||
private static byte[]? TryGetHellionFontBytes()
|
||||
{
|
||||
if (HellionFontBytes is not null)
|
||||
return HellionFontBytes;
|
||||
|
||||
using var stream =
|
||||
typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
|
||||
?? throw new FileNotFoundException(
|
||||
"Hellion font resource not embedded in the assembly"
|
||||
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
|
||||
"HellionFont.ttf"
|
||||
);
|
||||
if (stream is null)
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
"Hellion font resource missing — falling back to system default font."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
HellionFontBytes = ms.ToArray();
|
||||
@@ -146,8 +156,11 @@ public class FontManager
|
||||
? Plugin.Config.FontSizeV2
|
||||
: Plugin.Config.GlobalFontV2.SizePt;
|
||||
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
||||
config.MergeFont = Plugin.Config.UseHellionFont
|
||||
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
|
||||
// F10.2: if the embedded font is missing, drop to the system font
|
||||
// path rather than letting the UiBuilder throw.
|
||||
var hellionBytes = Plugin.Config.UseHellionFont ? TryGetHellionFontBytes() : null;
|
||||
config.MergeFont = hellionBytes is not null
|
||||
? tk.AddFontFromMemory(hellionBytes, config, "Hellion-Exo2")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||
|
||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||
@@ -213,11 +226,21 @@ public class FontManager
|
||||
return fontId.AddToBuildToolkit(tk, config);
|
||||
}
|
||||
catch (Exception e)
|
||||
when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
|
||||
when (e
|
||||
is FileNotFoundException
|
||||
or DirectoryNotFoundException
|
||||
or IOException
|
||||
or InvalidOperationException
|
||||
or ArgumentException
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
// Atlas-toolkit throws span IO and validation failures; routing the
|
||||
// wider set through the fallback keeps a corrupt font config from
|
||||
// taking down the whole atlas build.
|
||||
Plugin.LogProxy.Warning(
|
||||
e,
|
||||
$"Configured {slot} font unavailable, falling back to NotoSansCjkRegular"
|
||||
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
||||
+ "falling back to NotoSansCjkRegular"
|
||||
);
|
||||
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
|
||||
return fallback.AddToBuildToolkit(tk, config);
|
||||
|
||||
@@ -236,7 +236,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -266,7 +266,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
|
||||
return 1; // Prevent vanilla chat log from gaining focus
|
||||
@@ -299,7 +299,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||
worldId = agent->TellWorldId;
|
||||
Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}");
|
||||
Plugin.LogProxy.Debug($"Detected tell target '[redacted]'@{worldId}");
|
||||
}
|
||||
|
||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||
@@ -358,7 +358,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,16 +423,24 @@ internal sealed unsafe class Chat : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
// Check if channel is valid (non-linkshell or existing linkshell)
|
||||
internal static bool ValidAnyLinkshell(InputChannel channel)
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Renamed ValidAnyLinkshell -> IsChannelOrExistingLinkshell. The
|
||||
// name now states intent: returns true for any non-linkshell
|
||||
// channel, or a linkshell index that actually exists.
|
||||
// ---------------------------------------------------------------
|
||||
internal static bool IsChannelOrExistingLinkshell(InputChannel channel)
|
||||
{
|
||||
var idx = channel.LinkshellIndex();
|
||||
if (idx == uint.MaxValue || channel.IsExtraChatLinkshell())
|
||||
return true;
|
||||
if (channel.IsLinkshell() && ValidLinkshell(idx))
|
||||
return true;
|
||||
if (channel.IsCrossLinkshell() && ValidCrossLinkshell(idx))
|
||||
return true;
|
||||
|
||||
if (channel.IsLinkshell())
|
||||
return ValidLinkshell(idx);
|
||||
|
||||
if (channel.IsCrossLinkshell())
|
||||
return ValidCrossLinkshell(idx);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -531,12 +539,17 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (idx == uint.MaxValue)
|
||||
idx = 0;
|
||||
|
||||
if (!ValidAnyLinkshell(channel))
|
||||
return;
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Wrap ChangeChatChannel in the validity check instead of
|
||||
// early-returning. The previous early return skipped Dtor and
|
||||
// leaked the native Utf8String allocated a few lines above.
|
||||
// ---------------------------------------------------------------
|
||||
if (IsChannelOrExistingLinkshell(channel))
|
||||
RaptureShellModule
|
||||
.Instance()
|
||||
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||
|
||||
RaptureShellModule
|
||||
.Instance()
|
||||
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||
target->Dtor(true);
|
||||
}
|
||||
|
||||
@@ -611,7 +624,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (contentId == 0)
|
||||
{
|
||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -215,7 +215,7 @@ internal unsafe class GameFunctions : IDisposable
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Warning(e, "Unable to open adventurer plate");
|
||||
Plugin.LogProxy.Warning(e, "Unable to open adventurer plate");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,7 @@ internal unsafe class GameFunctions : IDisposable
|
||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||
if (byteCount >= PlaceholderBufferSize)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
||||
);
|
||||
ReplacementName = null;
|
||||
|
||||
@@ -507,7 +507,7 @@ internal unsafe class KeybindManager : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,5 +40,11 @@ public class TellTarget
|
||||
|
||||
public static TellTarget Empty() => new(string.Empty, 0, 0, TellReason.Direct);
|
||||
|
||||
public static TellTarget From(TellTarget t) => new(t.Name, t.World, t.ContentId, t.Reason);
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Replaced static From(t) with an instance-style Clone() so call
|
||||
// sites read like a copy operation, not a factory.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/TellTargetCloneTests.cs
|
||||
// ---------------------------------------------------------------
|
||||
public TellTarget Clone() => new(Name, World, ContentId, Reason);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||
<PropertyGroup>
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.4.4</Version>
|
||||
<Version>1.4.7</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
|
||||
+105
-26
@@ -35,6 +35,111 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
changelog: |-
|
||||
**v1.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, and a configurable
|
||||
sidebar.
|
||||
|
||||
- TempTell Pin: right-click a TempTell tab in the sidebar to pin
|
||||
it. Pinned tabs survive relog, keep their conversation history
|
||||
(loaded on demand from the message store), 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. New 'Pinned' section in the sidebar with its own divider
|
||||
header
|
||||
- Honorific Glow outline now renders when the title carries a
|
||||
Glow colour. Opt-in via Settings → Integrations → 'Render glow
|
||||
outlines (Honorific)' (default off, dodges the per-frame
|
||||
DrawList overhead on low-end hardware). Gradient (Color3 /
|
||||
GradientColourSet / Wave / Pulse) is parsed but rendered
|
||||
statically — a later cycle will port the full animation
|
||||
- Sidebar width is now configurable in Theme & Layout (range
|
||||
44–160 px). Default stays icon-only; widen to fit section
|
||||
headers like 'Active Tells (3)' without truncation
|
||||
- Settings Save no longer pops the chat input back to /tell with
|
||||
a pinned partner — 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
|
||||
- Util/ImGuiUtil.cs DrawArrows IconButton id now uses
|
||||
(id + 1).ToString() instead of the operator-precedence quirk
|
||||
id + 1.ToString() — generated IDs stay numerically stable
|
||||
- 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
|
||||
- Internal: TempTab counter switched from an Interlocked cached
|
||||
field to a derived Tabs.Count(predicate) — pin-state transitions
|
||||
are cold-path and don't need lock-free reads
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**
|
||||
|
||||
Maintenance patch. No user-visible behaviour changes; tightens the
|
||||
development feedback loop, fixes two upstream-inherited bugs, and
|
||||
prepares the code for the v1.4.7 backlog cleanup.
|
||||
|
||||
- preflight.sh gains a csharpier reflow check and a markdownlint
|
||||
pass so style drift and markdown violations are caught at the
|
||||
pre-push gate
|
||||
- FontManager fallback catches the full set of atlas-toolkit
|
||||
throws (IO, InvalidOperation, ArgumentException) — a corrupt
|
||||
font config no longer takes down the whole atlas build
|
||||
- BrandingLinks and IntegrationLinks URLs validated on plugin
|
||||
load — a typo in a future URL rotation now throws at startup
|
||||
- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel
|
||||
no longer leaks the native Utf8String when the linkshell check
|
||||
rejects the channel
|
||||
- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now
|
||||
deep-clones UsedChannel and TellTarget — PopOut and Temp tabs
|
||||
no longer mutate each other's channel state
|
||||
- Active-tab underline scales with DPI and rounds to physical
|
||||
pixels for crisp rendering above 100% scaling
|
||||
- IconButton width parameter no longer subtracts HUD-scaled
|
||||
padding from a raw int (measured width passes through verbatim)
|
||||
- Internal: HellionStyle ChildBgAlpha extracted to a testable
|
||||
helper; Plugin.SaveConfig clones only the temp tabs;
|
||||
SettingsOverview caches the draw-list per frame;
|
||||
Dalamud.Utility.Util surface routed through an IPlatformUtil
|
||||
indirection (MessageStore IsWine probe is now testable in
|
||||
isolation)
|
||||
- Built-in themes: Crystal Nocturne (sapphire and electric
|
||||
magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom.
|
||||
Users with Moonlit Bloom selected fall back to Hellion Arctic
|
||||
on first load
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
**v1.4.5 — UX and Robustness (2026-05-12)**
|
||||
|
||||
Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw
|
||||
failures surface as a notification, the first-run wizard has an
|
||||
explicit "Later" option, the input history clears on plugin reload,
|
||||
and the status bar version slot stops clipping in narrow windows.
|
||||
|
||||
- Chat window draw errors now show a one-shot notification instead
|
||||
of failing silently — stack trace stays in /xllog
|
||||
- First-run wizard: explicit "Later — keep defaults" button.
|
||||
Closing the X no longer silently accepts the defaults; the wizard
|
||||
reopens on the next plugin load if nothing was picked
|
||||
- InputHistoryService clears on plugin dispose so the previous
|
||||
session's typed commands don't bleed into the next load
|
||||
- Status bar hides the version slot when the chat window is too
|
||||
narrow to fit all five slots without overlap
|
||||
- Internal: explicit session-only Auto-Tell-Tab invariant in
|
||||
Plugin.cs plus a pinning test in the Build-Suite
|
||||
- Internal: FontManager falls back to the system font if the
|
||||
embedded Hellion font resource is missing — logs a Warning
|
||||
|
||||
---
|
||||
|
||||
**v1.4.4 — Threading and IPC safety polish (2026-05-12)**
|
||||
|
||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
|
||||
@@ -55,30 +160,4 @@ changelog: |-
|
||||
|
||||
---
|
||||
|
||||
**v1.4.3 — Faster plugin load + new repo (2026-05-08)**
|
||||
|
||||
Heavy startup work (migrations, hooks, windows) now runs async so
|
||||
Dalamud's UI stays responsive during load. Load time is comparable
|
||||
to v1.4.2 — this is the foundation for v1.4.4 optimisations.
|
||||
|
||||
- Two-phase async load via IAsyncDalamudPlugin
|
||||
- Schema-gate replaces the v9→v16 migration chain; old configs
|
||||
require a v1.4.2 install first
|
||||
- AutoTranslate cache loads on first use instead of every startup
|
||||
- Custom font (Hellion-Exo2) appears with a brief pop after load
|
||||
- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL
|
||||
|
||||
---
|
||||
|
||||
**v1.4.2 — Smoother frames in the chat log**
|
||||
|
||||
Per-frame allocations in the chat-log render path eliminated.
|
||||
2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.
|
||||
|
||||
- Card-mode: theme/border invariants hoisted out of the per-message loop
|
||||
- Auto-tell tab tint and icon cached per tab
|
||||
- Status bar aggregation runs on ~1% of frames instead of every frame
|
||||
|
||||
---
|
||||
|
||||
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace HellionChat;
|
||||
|
||||
// Shared input history for all ChatInputBars (main and pop-out windows).
|
||||
// Push deduplicates: existing entries are moved to the end when re-added.
|
||||
// TEST-MIRROR: ../../Hellion Build test/Util/InputHistoryServiceTests.cs
|
||||
public static class InputHistoryService
|
||||
{
|
||||
private const int MaxSize = 30;
|
||||
@@ -41,4 +42,12 @@ public static class InputHistoryService
|
||||
return null;
|
||||
return _entries[cursor];
|
||||
}
|
||||
|
||||
// Plugin reload doesn't reset static state automatically. Plugin.DisposeAsync
|
||||
// calls this so the next load starts with an empty history instead of
|
||||
// inheriting the previous session's entries.
|
||||
public static void Reset()
|
||||
{
|
||||
_entries.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,19 @@ namespace HellionChat.Integrations;
|
||||
|
||||
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||
// 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(
|
||||
string? Title,
|
||||
bool IsPrefix,
|
||||
bool IsOriginal,
|
||||
Vector3? Color
|
||||
Vector3? Color,
|
||||
Vector3? Glow,
|
||||
Vector3? Color3,
|
||||
int? GradientColourSet,
|
||||
string? GradientAnimationStyle
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
|
||||
@@ -5,4 +8,13 @@ internal static class IntegrationLinks
|
||||
{
|
||||
public const string HonorificRepo = "https://github.com/Caraxi/Honorific";
|
||||
public const string HonorificAuthor = "https://github.com/Caraxi";
|
||||
|
||||
// See BrandingLinks.ValidateUrls for the CA2255 rationale.
|
||||
#pragma warning disable CA2255
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255
|
||||
internal static void ValidateUrls()
|
||||
{
|
||||
UrlValidation.ValidateAll(nameof(IntegrationLinks), HonorificRepo, HonorificAuthor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,10 @@ public sealed class ExtraChat : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 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?)");
|
||||
Plugin.LogProxy.Verbose(
|
||||
ex,
|
||||
"ExtraChat IPC initial state query failed (peer not loaded?)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ public partial class Message
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed to parse extra chat channel GUID");
|
||||
Plugin.Log.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
||||
Plugin.LogProxy.Error(ex, "Failed to parse extra chat channel GUID");
|
||||
Plugin.LogProxy.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
||||
return Guid.Empty;
|
||||
}
|
||||
}
|
||||
@@ -251,7 +251,7 @@ public partial class Message
|
||||
AddChunkWithMessage(
|
||||
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}'"
|
||||
);
|
||||
}
|
||||
@@ -416,7 +416,7 @@ public partial class Message
|
||||
catch (Exception)
|
||||
{
|
||||
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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
{
|
||||
Plugin = plugin;
|
||||
|
||||
Store = new MessageStore(DatabasePath());
|
||||
Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil, Plugin.LogProxy);
|
||||
|
||||
PendingMessageThread = new Thread(() =>
|
||||
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||
@@ -91,7 +91,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
await Task.Delay(100);
|
||||
|
||||
if (PendingMessageThread.IsAlive)
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
"PendingMessageThread did not observe cancellation within 10s. "
|
||||
+ "Worker remains on background thread; next plugin reload releases it."
|
||||
);
|
||||
@@ -137,7 +137,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error processing pending message");
|
||||
Plugin.LogProxy.Error(ex, "Error processing pending message");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -182,10 +182,12 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
// Mark failed messages as deleted to prevent retry attempts
|
||||
var failedIds = messages.FailedMessageIds();
|
||||
Plugin.Log.Info($"Marking {failedIds.Count} messages as deleted due to parse failures");
|
||||
Plugin.LogProxy.Info(
|
||||
$"Marking {failedIds.Count} messages as deleted due to parse failures"
|
||||
);
|
||||
foreach (var msgId in messages.FailedMessageIds())
|
||||
{
|
||||
Plugin.Log.Debug($"Marking message '{msgId}' as deleted due to parse failure");
|
||||
Plugin.LogProxy.Debug($"Marking message '{msgId}' as deleted due to parse failure");
|
||||
Store.DeleteMessage(msgId);
|
||||
}
|
||||
}
|
||||
@@ -201,10 +203,10 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in FilterAllTabs");
|
||||
Plugin.LogProxy.Error(ex, "Error in FilterAllTabs");
|
||||
}
|
||||
|
||||
Plugin.Log.Debug($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||
Plugin.LogProxy.Debug($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,7 +261,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in ContentIdResolver");
|
||||
Plugin.LogProxy.Error(ex, "Error in ContentIdResolver");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-20
@@ -9,7 +9,6 @@ using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using DalamudUtil = Dalamud.Utility.Util;
|
||||
using Encoding = System.Text.Encoding;
|
||||
|
||||
namespace HellionChat;
|
||||
@@ -137,9 +136,14 @@ internal class MessageStore : IDisposable
|
||||
)
|
||||
);
|
||||
|
||||
internal MessageStore(string dbPath)
|
||||
private readonly IPlatformUtil _platformUtil;
|
||||
private readonly IPluginLogProxy _logger;
|
||||
|
||||
internal MessageStore(string dbPath, IPlatformUtil platformUtil, IPluginLogProxy logger)
|
||||
{
|
||||
DbPath = dbPath;
|
||||
_platformUtil = platformUtil;
|
||||
_logger = logger;
|
||||
Connection = Connect();
|
||||
Migrate();
|
||||
}
|
||||
@@ -166,7 +170,7 @@ internal class MessageStore : IDisposable
|
||||
conn.Open();
|
||||
conn.Execute(@"PRAGMA journal_mode=WAL;");
|
||||
conn.Execute(@"PRAGMA synchronous=NORMAL;");
|
||||
if (DalamudUtil.IsWine())
|
||||
if (_platformUtil.IsWine)
|
||||
conn.Execute(@"PRAGMA cache_size = 32768;");
|
||||
return conn;
|
||||
}
|
||||
@@ -202,7 +206,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate0()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 0: Creating tables");
|
||||
_logger.Information("Running migration 0: Creating tables");
|
||||
Connection.Execute(
|
||||
@"
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
@@ -229,7 +233,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate1()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 1: Adding Deleted column");
|
||||
_logger.Information("Running migration 1: Adding Deleted column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -241,7 +245,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate2()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 2: Adding Channel generated column");
|
||||
_logger.Information("Running migration 2: Adding Channel generated column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
|
||||
@@ -269,15 +273,13 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate3()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 3: Fix log kinds to fit the new format");
|
||||
_logger.Information("Running migration 3: Fix log kinds to fit the new format");
|
||||
|
||||
// Recovery for partially-applied Migrate3: schema already in target
|
||||
// shape but user_version was never bumped -- just record and exit.
|
||||
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
|
||||
{
|
||||
Plugin.Log.Information(
|
||||
"Migration 3: schema already migrated, only bumping user_version"
|
||||
);
|
||||
_logger.Information("Migration 3: schema already migrated, only bumping user_version");
|
||||
SetMigrationVersion(3);
|
||||
return;
|
||||
}
|
||||
@@ -307,7 +309,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void SetMigrationVersion(int version)
|
||||
{
|
||||
Plugin.Log.Information($"Setting version {version}");
|
||||
_logger.Information($"Setting version {version}");
|
||||
using var cmd = Connection.CreateCommand();
|
||||
// PRAGMA does not accept SQLite parameter bindings; version is a
|
||||
// compile-time int from the migration sequence, never user input.
|
||||
@@ -459,7 +461,7 @@ internal class MessageStore : IDisposable
|
||||
// Privacy filter -- drop disallowed ChatTypes before they reach storage.
|
||||
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
||||
{
|
||||
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||
_logger.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,7 +554,7 @@ internal class MessageStore : IDisposable
|
||||
if (to is not null)
|
||||
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
// Returns the most recent messages, oldest-first.
|
||||
@@ -600,7 +602,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
cmd.Parameters.AddWithValue("$Count", count);
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
// Returns up to limit tells exchanged with the named player, oldest-first.
|
||||
@@ -638,7 +640,7 @@ internal class MessageStore : IDisposable
|
||||
cmd.Parameters.AddWithValue("$ScanLimit", sqlScanLimit);
|
||||
|
||||
var collected = new List<Message>();
|
||||
using var enumerator = new MessageEnumerator(cmd.ExecuteReader());
|
||||
using var enumerator = new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
foreach (var message in enumerator)
|
||||
{
|
||||
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
|
||||
@@ -730,7 +732,7 @@ internal class MessageStore : IDisposable
|
||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
internal MessageEnumerator GetPagedDateRange(
|
||||
@@ -774,7 +776,7 @@ internal class MessageStore : IDisposable
|
||||
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
||||
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
// Builds a "$prefix0,$prefix1,..." placeholder list and binds values to the command.
|
||||
@@ -794,13 +796,14 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal class MessageEnumerator(DbDataReader reader)
|
||||
internal class MessageEnumerator(DbDataReader reader, IPluginLogProxy logger)
|
||||
: IEnumerable<Message>,
|
||||
IDisposable,
|
||||
IAsyncDisposable
|
||||
{
|
||||
private const int MaxErrorLogs = 10;
|
||||
|
||||
private readonly IPluginLogProxy _logger = logger;
|
||||
private readonly List<Guid> FailedIds = [];
|
||||
private int FailedCount;
|
||||
public bool DidError => FailedCount > 0;
|
||||
@@ -846,10 +849,10 @@ internal class MessageEnumerator(DbDataReader reader)
|
||||
catch (Exception e)
|
||||
{
|
||||
if (FailedCount < MaxErrorLogs)
|
||||
Plugin.Log.Error($"Exception while reading message '{id}' from database: {e}");
|
||||
_logger.Error($"Exception while reading message '{id}' from database: {e}");
|
||||
FailedCount++;
|
||||
if (FailedCount == MaxErrorLogs)
|
||||
Plugin.Log.Error("Further parsing errors will not be logged");
|
||||
_logger.Error("Further parsing errors will not be logged");
|
||||
if (id != Guid.Empty)
|
||||
FailedIds.Add(id);
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ public sealed class PayloadHandler
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error executing integration");
|
||||
Plugin.LogProxy.Error(ex, "Error executing integration");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +535,7 @@ public sealed class PayloadHandler
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Could not find DalamudLinkHandlers");
|
||||
Plugin.LogProxy.Warning("Could not find DalamudLinkHandlers");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@ public sealed class PayloadHandler
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error executing DalamudLinkPayload handler");
|
||||
Plugin.LogProxy.Error(ex, "Error executing DalamudLinkPayload handler");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-16
@@ -113,6 +113,15 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
||||
|
||||
// Platform indirection over Dalamud.Utility.Util. Wired in Phase-1 ctor so
|
||||
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
||||
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!;
|
||||
|
||||
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||
private int _disposeStarted;
|
||||
|
||||
@@ -154,18 +163,28 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
|
||||
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
||||
// must install v1.4.2 first to run the migration chain.
|
||||
// Wire platform indirection before LoadAsync allocates anything that
|
||||
// needs Util.* — services then read Plugin.PlatformUtil instead of
|
||||
// hitting the Dalamud static surface directly.
|
||||
PlatformUtil = new DalamudPlatformUtil();
|
||||
LogProxy = new DalamudPluginLogProxy(Log);
|
||||
|
||||
// Schema gate: v1.4.x requires config v16+. Users on older schemas
|
||||
// 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)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"HellionChat v1.4.4 requires config schema v16, got v{Config.Version}. "
|
||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.4."
|
||||
$"HellionChat v1.4.7 requires config schema v16, got v{Config.Version}. "
|
||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.7."
|
||||
);
|
||||
}
|
||||
Config.Version = 17;
|
||||
|
||||
// Drop session-only Auto-Tell-Tabs that a previous crash may have persisted.
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
|
||||
|
||||
LanguageChanged(Interface.UiLanguage);
|
||||
ImGuiUtil.Initialize(this);
|
||||
@@ -372,6 +391,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
||||
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
||||
// Static input history would otherwise survive the plugin reload.
|
||||
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
||||
|
||||
if (failure is not null)
|
||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||
@@ -633,19 +654,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
internal void SaveConfig()
|
||||
{
|
||||
// Strip session-only Auto-Tell-Tabs before serialization; restore after.
|
||||
var snapshot = Config.Tabs.ToList();
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
// Only unpinned TempTabs are session-only — they move aside before
|
||||
// serialization and re-attach after. Pinned TempTabs stay in
|
||||
// Config.Tabs across the save so JSON includes them. Cloning only the
|
||||
// unpinned subset keeps the allocation proportional to
|
||||
// AutoTellTabsLimit (<=15) instead of the full tab list.
|
||||
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||
|
||||
Interface.SavePluginConfig(Config);
|
||||
|
||||
Config.Tabs.Clear();
|
||||
Config.Tabs.AddRange(snapshot);
|
||||
|
||||
// F2.1: snapshot-restore preserves IsTempTab tabs but the mid-step
|
||||
// RemoveAll bypasses AutoTellTabsService, so re-peg the counter.
|
||||
// Null-conditional because SaveConfig can fire before Phase-2 init.
|
||||
AutoTellTabsService?.ResyncTempTabCounter();
|
||||
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||
}
|
||||
|
||||
internal void LanguageChanged(string langCode)
|
||||
|
||||
+14
@@ -114,6 +114,8 @@ internal class HellionStrings
|
||||
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
|
||||
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
|
||||
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
|
||||
internal static string Wizard_Cancel_Label => Get(nameof(Wizard_Cancel_Label));
|
||||
internal static string Wizard_Cancel_Tooltip => Get(nameof(Wizard_Cancel_Tooltip));
|
||||
|
||||
internal static string Export_Heading => Get(nameof(Export_Heading));
|
||||
internal static string Export_Help => Get(nameof(Export_Help));
|
||||
@@ -168,6 +170,16 @@ internal class HellionStrings
|
||||
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
||||
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
||||
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
|
||||
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
||||
@@ -368,6 +380,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_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_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_LinkAuthor => Get(nameof(Settings_Integrations_Honorific_LinkAuthor));
|
||||
internal static string Settings_Integrations_ComingSoon_SectionHeader => Get(nameof(Settings_Integrations_ComingSoon_SectionHeader));
|
||||
|
||||
@@ -222,6 +222,12 @@
|
||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||
<value>Wizard erneut zeigen</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Label" xml:space="preserve">
|
||||
<value>Später — Defaults behalten</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||
<value>Schließt den Wizard ohne Profil-Auswahl. Die Plugin-Defaults bleiben aktiv und der Wizard erscheint beim nächsten Plugin-Reload erneut.</value>
|
||||
</data>
|
||||
<data name="Export_Heading" xml:space="preserve">
|
||||
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
|
||||
</data>
|
||||
@@ -377,6 +383,36 @@
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Als begrüßt markieren.</value>
|
||||
</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) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
@@ -392,7 +428,7 @@
|
||||
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<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 name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Kompakte Anzeige</value>
|
||||
@@ -821,6 +857,12 @@
|
||||
<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>
|
||||
</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">
|
||||
<value>Honorific auf GitHub</value>
|
||||
</data>
|
||||
|
||||
@@ -222,6 +222,12 @@
|
||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||
<value>Show wizard again</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Label" xml:space="preserve">
|
||||
<value>Later — keep defaults</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||
<value>Close the wizard without selecting a profile. The plugin defaults stay active and the wizard returns on next plugin load.</value>
|
||||
</data>
|
||||
<data name="Export_Heading" xml:space="preserve">
|
||||
<value>Export (GDPR Art. 15 — Right of access)</value>
|
||||
</data>
|
||||
@@ -377,6 +383,36 @@
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Mark as greeted.</value>
|
||||
</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) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
@@ -392,7 +428,7 @@
|
||||
<value>Maximum number of auto-tell tabs</value>
|
||||
</data>
|
||||
<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 name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Compact display</value>
|
||||
@@ -821,6 +857,12 @@
|
||||
<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>
|
||||
</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">
|
||||
<value>Honorific on GitHub</value>
|
||||
</data>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Themes.Builtin;
|
||||
|
||||
internal static class CrystalNocturne
|
||||
{
|
||||
public const string Slug = "crystal-nocturne";
|
||||
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Crystal Nocturne",
|
||||
Author: "CRYSTALLITE",
|
||||
Description: "Royal sapphire and electric magenta over obsidian — a nocturne for the crystal-lit dance floor.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#1D4ED8"),
|
||||
Primary: ColourUtil.HexToRgba("#3B82F6"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#93C5FD"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#3B82F699"),
|
||||
AccentDark: ColourUtil.HexToRgba("#A21CAF"),
|
||||
Accent: ColourUtil.HexToRgba("#D946EF"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F0ABFC"),
|
||||
Identity: ColourUtil.HexToRgba("#3B82F6"),
|
||||
WindowBg: ColourUtil.HexToRgba("#08070F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#11101F"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1C1A33"),
|
||||
Surface: ColourUtil.HexToRgba("#262340"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#332D55"),
|
||||
Border: ColourUtil.HexToRgba("#D946EF55"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#F5F3FF"),
|
||||
TextMuted: ColourUtil.HexToRgba("#A5A0C0"),
|
||||
TextDim: ColourUtil.HexToRgba("#4B4763"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#10B981"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#F43F5E"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FACC15"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#3B82F6")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 2f,
|
||||
ChildRounding: 1f,
|
||||
PopupRounding: 2f,
|
||||
FrameRounding: 1f,
|
||||
GrabRounding: 1f,
|
||||
TabRounding: 1f,
|
||||
ScrollbarRounding: 2f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Crystal Nocturne — sapphire-blue identity for party/team channels,
|
||||
// accent-magenta for tells, with mint/peach accents on linkshells
|
||||
// so the eight LS slots stay individually distinguishable on the
|
||||
// dark obsidian background.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F5F3FF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FACC15"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#93C5FD"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#10B981"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#93C5FD"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#10B981"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FACC15"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#93C5FD"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A5A0C0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#D946EF"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#3B82F6"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A5A0C0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Themes.Builtin;
|
||||
|
||||
internal static class MoonlitBloom
|
||||
{
|
||||
public const string Slug = "moonlit-bloom";
|
||||
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Moonlit Bloom",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
|
||||
Primary: ColourUtil.HexToRgba("#E374E8"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
|
||||
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
Accent: ColourUtil.HexToRgba("#9CCB7C"),
|
||||
AccentLight: ColourUtil.HexToRgba("#B6E297"),
|
||||
Identity: ColourUtil.HexToRgba("#E374E8"),
|
||||
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#15122B"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
|
||||
Surface: ColourUtil.HexToRgba("#28224A"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
|
||||
Border: ColourUtil.HexToRgba("#E374E844"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
|
||||
TextDim: ColourUtil.HexToRgba("#554B6E"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#6278FF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f,
|
||||
ChildRounding: 5f,
|
||||
PopupRounding: 5f,
|
||||
FrameRounding: 4f,
|
||||
GrabRounding: 4f,
|
||||
TabRounding: 4f,
|
||||
ScrollbarRounding: 4f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
|
||||
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -15,17 +15,21 @@ public sealed class ThemeRegistry
|
||||
|
||||
public ThemeRegistry(string? customThemesDir = null)
|
||||
{
|
||||
// Insertion order drives the Theme-Picker grid layout (3 columns).
|
||||
// Row 1: blue family. Row 2: purple to magenta family.
|
||||
// Row 3: green / warm / classic. Row 4: Synthwave Sunset as a
|
||||
// retro bonus on its own line.
|
||||
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
||||
{ NightBlue.Slug, NightBlue.Build() },
|
||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||
{ CrystalNocturne.Slug, CrystalNocturne.Build() },
|
||||
{ MintGrove.Slug, MintGrove.Build() },
|
||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
||||
};
|
||||
|
||||
@@ -114,7 +118,7 @@ public sealed class ThemeRegistry
|
||||
catch (Exception ex) when (IsRecoverableFileLock(ex))
|
||||
{
|
||||
// Editor mid-save: keep last known good, retry on next refresh.
|
||||
Plugin.Log.Debug(
|
||||
Plugin.LogProxy.Debug(
|
||||
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
|
||||
);
|
||||
if (cached.Theme is not null)
|
||||
|
||||
+208
-32
@@ -90,6 +90,10 @@ public sealed class ChatLogWindow : Window
|
||||
private bool PlayedClosingSound = true;
|
||||
private bool DrewThisFrame;
|
||||
|
||||
// One-shot guard so a recurring draw failure doesn't spam the
|
||||
// notification stack frame-by-frame. Resets only on next plugin reload.
|
||||
private bool NotifiedDrawFailure;
|
||||
|
||||
private long FrameTime; // set every frame
|
||||
internal long LastActivityTime = Environment.TickCount64;
|
||||
|
||||
@@ -268,9 +272,12 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
if (targetChannel == null || !GameFunctions.Chat.ValidAnyLinkshell(targetChannel.Value))
|
||||
if (
|
||||
targetChannel == null
|
||||
|| !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
||||
);
|
||||
return;
|
||||
@@ -324,11 +331,11 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
case "hide":
|
||||
CurrentHideState = HideState.User;
|
||||
Plugin.Log.Verbose("HideState: → User (chat hide command)");
|
||||
Plugin.LogProxy.Verbose("HideState: → User (chat hide command)");
|
||||
break;
|
||||
case "show":
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose("HideState: → None (chat show command)");
|
||||
Plugin.LogProxy.Verbose("HideState: → None (chat show command)");
|
||||
break;
|
||||
case "toggle":
|
||||
CurrentHideState = CurrentHideState switch
|
||||
@@ -338,7 +345,7 @@ public sealed class ChatLogWindow : Window
|
||||
HideState.None => HideState.User,
|
||||
_ => CurrentHideState,
|
||||
};
|
||||
Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||
Plugin.LogProxy.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -434,11 +441,24 @@ public sealed class ChatLogWindow : Window
|
||||
|
||||
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)
|
||||
{
|
||||
newTab.CurrentChannel.Channel = newTab.Channel.Value;
|
||||
}
|
||||
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
|
||||
newTab.CurrentChannel = previousTab.CurrentChannel;
|
||||
{
|
||||
newTab.CurrentChannel = previousTab.CurrentChannel.Clone();
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' "
|
||||
+ $"(Channel={newTab.CurrentChannel.Channel}, TellTarget={newTab.CurrentChannel.TellTarget?.ToTargetString() ?? "null"})"
|
||||
);
|
||||
}
|
||||
|
||||
SetChannel(newTab.CurrentChannel.Channel);
|
||||
}
|
||||
@@ -462,14 +482,14 @@ public sealed class ChatLogWindow : Window
|
||||
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.Battle;
|
||||
Plugin.Log.Verbose("HideState: None → Battle");
|
||||
Plugin.LogProxy.Verbose("HideState: None → Battle");
|
||||
}
|
||||
|
||||
// If the chat is hidden because of battle, we reset it here
|
||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose("HideState: Battle → None");
|
||||
Plugin.LogProxy.Verbose("HideState: Battle → None");
|
||||
}
|
||||
|
||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||
@@ -482,7 +502,7 @@ public sealed class ChatLogWindow : Window
|
||||
if (Plugin.Functions.Chat.CheckHideFlags())
|
||||
{
|
||||
CurrentHideState = HideState.Cutscene;
|
||||
Plugin.Log.Verbose("HideState: None → Cutscene");
|
||||
Plugin.LogProxy.Verbose("HideState: None → Cutscene");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +513,7 @@ public sealed class ChatLogWindow : Window
|
||||
&& !Plugin.GposeActive
|
||||
)
|
||||
{
|
||||
Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||
Plugin.LogProxy.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||
CurrentHideState = HideState.None;
|
||||
}
|
||||
|
||||
@@ -501,14 +521,14 @@ public sealed class ChatLogWindow : Window
|
||||
if (CurrentHideState == HideState.Cutscene && Activate)
|
||||
{
|
||||
CurrentHideState = HideState.CutsceneOverride;
|
||||
Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||
Plugin.LogProxy.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||
}
|
||||
|
||||
// if the user hid the chat and is now activating chat, reset the hide state
|
||||
if (CurrentHideState == HideState.User && Activate)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose("HideState: User → None (activate)");
|
||||
Plugin.LogProxy.Verbose("HideState: User → None (activate)");
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -626,7 +646,20 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error drawing Chat Log window");
|
||||
Plugin.LogProxy.Error(ex, "Error drawing Chat Log window");
|
||||
if (!NotifiedDrawFailure)
|
||||
{
|
||||
Plugin.Notification.AddNotification(
|
||||
new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = "Hellion Chat",
|
||||
Content = "A drawing error occurred. Check /xllog for details.",
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
InitialDuration = TimeSpan.FromSeconds(20),
|
||||
}
|
||||
);
|
||||
NotifiedDrawFailure = true;
|
||||
}
|
||||
// Prevent recurring draw failures from constantly trying to grab
|
||||
// input focus, which breaks every other ImGui window.
|
||||
Activate = false;
|
||||
@@ -1588,7 +1621,7 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Warning(ex, "Error drawing chat log");
|
||||
Plugin.LogProxy.Warning(ex, "Error drawing chat log");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1620,17 +1653,21 @@ public sealed class ChatLogWindow : Window
|
||||
continue;
|
||||
|
||||
// Active-tab underline pill (2px accent). No native ImGui underline API,
|
||||
// so we use a direct DrawList pass.
|
||||
// so we use a direct DrawList pass. Pill height scales with GlobalScale
|
||||
// and all coordinates round to physical pixels so the line stays crisp
|
||||
// on 125/150% DPI setups instead of bleeding into a sub-pixel blur.
|
||||
{
|
||||
var theme = Plugin.ThemeRegistry.Active;
|
||||
var min = ImGui.GetItemRectMin();
|
||||
var max = ImGui.GetItemRectMax();
|
||||
const float pillHeight = 2f;
|
||||
var pillHeight = MathF.Max(1f, MathF.Round(2f * ImGuiHelpers.GlobalScale));
|
||||
var yBottom = MathF.Round(max.Y);
|
||||
var yTop = yBottom - pillHeight;
|
||||
ImGui
|
||||
.GetWindowDrawList()
|
||||
.AddRectFilled(
|
||||
new Vector2(min.X, max.Y - pillHeight),
|
||||
new Vector2(max.X, max.Y),
|
||||
new Vector2(MathF.Round(min.X), yTop),
|
||||
new Vector2(MathF.Round(max.X), yBottom),
|
||||
ColourUtil.RgbaToAbgr(theme.Colors.Accent)
|
||||
);
|
||||
}
|
||||
@@ -1649,6 +1686,30 @@ public sealed class ChatLogWindow : Window
|
||||
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()
|
||||
{
|
||||
var currentTab = -1;
|
||||
@@ -1661,7 +1722,8 @@ public sealed class ChatLogWindow : Window
|
||||
if (!tabTable.Success)
|
||||
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.TableNextColumn();
|
||||
@@ -1680,23 +1742,42 @@ public sealed class ChatLogWindow : Window
|
||||
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
|
||||
|
||||
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 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];
|
||||
if (tab.PopOut)
|
||||
continue;
|
||||
|
||||
if (tab.IsTempTab && !tempTabHeaderRendered)
|
||||
if (TabLifecycleHelpers.IsInPinnedPool(tab) && !pinnedHeaderRendered)
|
||||
{
|
||||
ImGui.Separator();
|
||||
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||
{
|
||||
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;
|
||||
@@ -1785,9 +1866,12 @@ public sealed class ChatLogWindow : Window
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
|
||||
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(
|
||||
$"{icon.ToIconString()}##sidebar-tab-{tabI}",
|
||||
new Vector2(36f, ImGui.GetFrameHeight())
|
||||
new Vector2(sidebarWidth - 8f, ImGui.GetFrameHeight())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1847,11 +1931,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.
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using var tt = ImRaii.Tooltip();
|
||||
ImGui.TextUnformatted($"{tab.Name}{unread}");
|
||||
if (tab.IsPinned)
|
||||
{
|
||||
ImGui.TextUnformatted(HellionStrings.PinTab_PinnedTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
DrawTabContextMenu(tab, tabI);
|
||||
@@ -1975,10 +2083,7 @@ public sealed class ChatLogWindow : Window
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
||||
}
|
||||
ImGui.SameLine(0f, gapAfterCrown);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
||||
{
|
||||
ImGui.TextUnformatted(rendered);
|
||||
}
|
||||
DrawHonorificTitleText(rendered, titleColor, title.Glow);
|
||||
ImGui.EndGroup();
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
@@ -1989,6 +2094,35 @@ public sealed class ChatLogWindow : Window
|
||||
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.
|
||||
private float DrawV061HintBannerIfNeeded()
|
||||
{
|
||||
@@ -2035,7 +2169,7 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
Plugin.Config.SeenPopOutHeaderHint = true;
|
||||
Plugin.SaveConfig();
|
||||
Plugin.Log.Debug("v0.6.1 pop-out header hint dismissed");
|
||||
Plugin.LogProxy.Debug("v0.6.1 pop-out header hint dismissed");
|
||||
if (openSettings)
|
||||
Plugin.SettingsWindow.Toggle();
|
||||
}
|
||||
@@ -2100,10 +2234,52 @@ public sealed class ChatLogWindow : Window
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
if (tab.IsTempTab)
|
||||
{
|
||||
ImGui.Separator();
|
||||
DrawPinControls(tab);
|
||||
}
|
||||
|
||||
if (anyChanged)
|
||||
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 HashSet<Guid> PopOutWindows = [];
|
||||
|
||||
@@ -2648,7 +2824,7 @@ public sealed class ChatLogWindow : Window
|
||||
var viewport = ImGui.GetMainViewport();
|
||||
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
||||
Position = safePos;
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
|
||||
);
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ public class DbViewer : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed reading messages from database");
|
||||
Plugin.LogProxy.Error(ex, "Failed reading messages from database");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -570,7 +570,7 @@ public class DbViewer : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed creating txt backup");
|
||||
Plugin.LogProxy.Error(ex, "Failed creating txt backup");
|
||||
|
||||
Notification.Content = "Error ...";
|
||||
Notification.Type = NotificationType.Error;
|
||||
|
||||
@@ -30,14 +30,10 @@ public sealed class FirstRunWizard : Window
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
// Closing the wizard without picking anything = the user accepts
|
||||
// whatever defaults are already in place. Mark as complete so we
|
||||
// don't pester them again on the next launch.
|
||||
if (!Plugin.Config.FirstRunCompleted)
|
||||
{
|
||||
Plugin.Config.FirstRunCompleted = true;
|
||||
Plugin.SaveConfig();
|
||||
}
|
||||
// OnClose fires on explicit X-click and on plugin dispose. We never
|
||||
// implicitly accept the defaults here — the explicit "Later" button
|
||||
// does that. If the user hasn't picked a profile yet, the wizard
|
||||
// reopens on the next plugin load.
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
@@ -49,7 +45,12 @@ public sealed class FirstRunWizard : Window
|
||||
|
||||
var avail = ImGui.GetContentRegionAvail();
|
||||
var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f;
|
||||
var cardHeight = avail.Y - ImGui.GetTextLineHeightWithSpacing();
|
||||
// Reserve room for the footer separator + cancel button below the cards.
|
||||
var footerReserve =
|
||||
ImGui.GetStyle().ItemSpacing.Y * 3
|
||||
+ ImGui.GetTextLineHeight()
|
||||
+ ImGui.GetFrameHeightWithSpacing();
|
||||
var cardHeight = avail.Y - footerReserve;
|
||||
|
||||
DrawCard(
|
||||
"privacy-first",
|
||||
@@ -87,6 +88,20 @@ public sealed class FirstRunWizard : Window
|
||||
HellionStrings.Wizard_Profile_FullHistory_Apply,
|
||||
ApplyFullHistory
|
||||
);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGui.Button(HellionStrings.Wizard_Cancel_Label))
|
||||
{
|
||||
Plugin.Config.FirstRunCompleted = true;
|
||||
Plugin.SaveConfig();
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGuiUtil.Tooltip(HellionStrings.Wizard_Cancel_Tooltip);
|
||||
}
|
||||
|
||||
private void DrawCard(
|
||||
|
||||
@@ -43,13 +43,10 @@ internal static class HellionStyle
|
||||
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
||||
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
||||
|
||||
// ChildBg alpha: child areas rendered inside ChatLogWindow would
|
||||
// multiply their alpha with WindowBg, making 50% opacity appear
|
||||
// ~75% solid. At full opacity the theme's alpha is preserved; below
|
||||
// it ChildBg goes fully transparent so only WindowBg sets the final
|
||||
// coverage.
|
||||
var childBgAlpha = windowOpacity >= 0.999f ? (c.ChildBg & 0xFFu) : 0u;
|
||||
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | childBgAlpha;
|
||||
// ChildBg alpha resolution lives in HellionStyleHelpers so the
|
||||
// threshold logic can be covered by a pure-helper test in the
|
||||
// build suite.
|
||||
var childBgWithAlpha = HellionStyleHelpers.ResolveChildBgAlpha(c.ChildBg, windowOpacity);
|
||||
|
||||
// Layout
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
internal static class HellionStyleHelpers
|
||||
{
|
||||
// Child surfaces are drawn over WindowBg, so at partial window opacity
|
||||
// the theme's own ChildBg alpha would double-multiply and read too solid.
|
||||
// Above ~full opacity we preserve the theme alpha; below it we wipe to 0
|
||||
// so WindowBg alone carries the coverage. The 0.999f threshold is a
|
||||
// float-imprecision guard around the user-facing 100% slider value.
|
||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/HellionStyleHelpersTests.cs
|
||||
public static uint ResolveChildBgAlpha(uint themeChildBgRgba, float windowOpacity)
|
||||
{
|
||||
var alphaPreserved = windowOpacity >= 0.999f;
|
||||
var childBgAlpha = alphaPreserved ? (themeChildBgRgba & 0xFFu) : 0u;
|
||||
return (themeChildBgRgba & 0xFFFFFF00u) | childBgAlpha;
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ internal class Popout : Window
|
||||
{
|
||||
Plugin.Config.SeenPopOutInputHint = true;
|
||||
ChatLogWindow.Plugin.SaveConfig();
|
||||
Plugin.Log.Debug("Pop-Out input hint dismissed");
|
||||
Plugin.LogProxy.Debug("Pop-Out input hint dismissed");
|
||||
if (openSettings)
|
||||
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
||||
}
|
||||
@@ -214,13 +214,13 @@ internal class Popout : Window
|
||||
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.Battle;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle");
|
||||
}
|
||||
|
||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -232,7 +232,7 @@ internal class Popout : Window
|
||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||
{
|
||||
CurrentHideState = HideState.Cutscene;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ internal class Popout : Window
|
||||
&& !Plugin.GposeActive
|
||||
)
|
||||
{
|
||||
Plugin.Log.Verbose(
|
||||
Plugin.LogProxy.Verbose(
|
||||
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
|
||||
);
|
||||
CurrentHideState = HideState.None;
|
||||
@@ -251,7 +251,7 @@ internal class Popout : Window
|
||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||
{
|
||||
CurrentHideState = HideState.CutsceneOverride;
|
||||
Plugin.Log.Verbose(
|
||||
Plugin.LogProxy.Verbose(
|
||||
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
|
||||
);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ internal class Popout : Window
|
||||
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
||||
}
|
||||
|
||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
|
||||
|
||||
@@ -199,12 +199,12 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
);
|
||||
|
||||
if (ImGui.Button(buttonLabel2))
|
||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
|
||||
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/infiii");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button(buttonLabel))
|
||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/lojewalo");
|
||||
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/lojewalo");
|
||||
}
|
||||
|
||||
if (!save)
|
||||
|
||||
@@ -79,11 +79,13 @@ internal sealed class SettingsOverview
|
||||
// 110f accommodates two-line subtexts; wrap width is matched in DrawCard.
|
||||
var cardHeight = 110f;
|
||||
|
||||
// One draw-list lookup per frame instead of one per card.
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var cardDefs = BuildCardDefs();
|
||||
for (var i = 0; i < cardDefs.Length; i++)
|
||||
{
|
||||
var (icon, title, subtext) = cardDefs[i];
|
||||
DrawCard(i, icon, title, subtext, cardWidth, cardHeight);
|
||||
DrawCard(i, icon, title, subtext, cardWidth, cardHeight, drawList);
|
||||
|
||||
if ((i + 1) % columns != 0 && i != cardDefs.Length - 1)
|
||||
ImGui.SameLine();
|
||||
@@ -96,7 +98,8 @@ internal sealed class SettingsOverview
|
||||
string title,
|
||||
string subtext,
|
||||
float w,
|
||||
float h
|
||||
float h,
|
||||
ImDrawListPtr drawList
|
||||
)
|
||||
{
|
||||
// BeginGroup makes the card a single layout item so SameLine works
|
||||
@@ -108,8 +111,7 @@ internal sealed class SettingsOverview
|
||||
var hovered = ImGui.IsItemHovered();
|
||||
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
||||
|
||||
var draw = ImGui.GetWindowDrawList();
|
||||
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||
drawList.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||
|
||||
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
||||
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
||||
@@ -120,15 +122,15 @@ internal sealed class SettingsOverview
|
||||
|
||||
using (_window.Plugin.FontManager.FontAwesome.Push())
|
||||
{
|
||||
draw.AddText(iconPos, titleColor, icon.ToIconString());
|
||||
drawList.AddText(iconPos, titleColor, icon.ToIconString());
|
||||
}
|
||||
|
||||
draw.AddText(titlePos, titleColor, title);
|
||||
drawList.AddText(titlePos, titleColor, title);
|
||||
|
||||
// Subtext wraps at card inner width (16px padding each side) via DrawList
|
||||
// to avoid expanding the group bounds and breaking SameLine in the card row.
|
||||
var subtextWrapWidth = w - 32f;
|
||||
draw.AddText(
|
||||
drawList.AddText(
|
||||
ImGui.GetFont(),
|
||||
ImGui.GetFontSize(),
|
||||
subtextPos,
|
||||
|
||||
@@ -229,7 +229,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Unable to delete old database");
|
||||
Plugin.LogProxy.Error(e, "Unable to delete old database");
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Old_Delete_Error,
|
||||
NotificationType.Error
|
||||
@@ -391,7 +391,9 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||
Plugin.SaveConfig();
|
||||
|
||||
Plugin.Log.Information($"Manual retention run deleted {deleted} expired messages.");
|
||||
Plugin.LogProxy.Information(
|
||||
$"Manual retention run deleted {deleted} expired messages."
|
||||
);
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
@@ -405,7 +407,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
"Retention sweep: framework refresh timed out after 5s."
|
||||
);
|
||||
}
|
||||
@@ -418,7 +420,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Manual retention run failed");
|
||||
Plugin.LogProxy.Error(e, "Manual retention run failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
@@ -566,7 +568,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Failed to compute cleanup preview");
|
||||
Plugin.LogProxy.Error(e, "Failed to compute cleanup preview");
|
||||
WrapperUtil.AddNotification(
|
||||
HellionStrings.Cleanup_PreviewError,
|
||||
NotificationType.Error
|
||||
@@ -587,7 +589,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
try
|
||||
{
|
||||
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
||||
Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages");
|
||||
Plugin.LogProxy.Information($"Privacy cleanup: deleted {deleted} messages");
|
||||
|
||||
if (
|
||||
!Plugin
|
||||
@@ -599,7 +601,9 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||
Plugin.LogProxy.Warning(
|
||||
"Privacy cleanup: framework refresh timed out after 5s."
|
||||
);
|
||||
}
|
||||
|
||||
WrapperUtil.AddNotification(
|
||||
@@ -609,7 +613,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Privacy cleanup failed");
|
||||
Plugin.LogProxy.Error(e, "Privacy cleanup failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
@@ -769,7 +773,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Export failed");
|
||||
Plugin.LogProxy.Error(e, "Export failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
@@ -849,7 +853,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Clearing messages from database");
|
||||
Plugin.LogProxy.Warning("Clearing messages from database");
|
||||
Plugin.MessageManager.Store.ClearMessages();
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
|
||||
@@ -907,7 +911,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
private void InsertMessages(int count)
|
||||
{
|
||||
Plugin.Log.Info($"Inserting {count} messages due to user request");
|
||||
Plugin.LogProxy.Info($"Inserting {count} messages due to user request");
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var playerName = Plugin.PlayerState.CharacterName;
|
||||
@@ -952,7 +956,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
var elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
@@ -962,7 +966,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
@@ -973,7 +977,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
})
|
||||
@@ -986,7 +990,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.MessageManager.FilterAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
})
|
||||
|
||||
@@ -312,6 +312,6 @@ internal sealed class FontsAndColours : ISettingsTab
|
||||
}
|
||||
Plugin.SaveConfig();
|
||||
GlobalParametersCache.Refresh();
|
||||
Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
||||
Plugin.LogProxy.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ internal sealed class Information : ISettingsTab
|
||||
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
||||
Dalamud.Utility.Util.OpenLink(
|
||||
Plugin.PlatformUtil.OpenLink(
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues"
|
||||
);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ internal sealed class Information : ISettingsTab
|
||||
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
||||
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
|
||||
Plugin.PlatformUtil.OpenLink("https://hellion-media.de");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
@@ -137,7 +137,7 @@ internal sealed class Information : ISettingsTab
|
||||
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
||||
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
||||
Plugin.PlatformUtil.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
|
||||
@@ -71,6 +71,17 @@ internal sealed class Integrations : ISettingsTab
|
||||
{
|
||||
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
|
||||
@@ -79,12 +90,12 @@ internal sealed class Integrations : ISettingsTab
|
||||
ImGui.Spacing();
|
||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo))
|
||||
{
|
||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificRepo);
|
||||
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificRepo);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkAuthor))
|
||||
{
|
||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificAuthor);
|
||||
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificAuthor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +204,7 @@ internal sealed class Integrations : ISettingsTab
|
||||
|
||||
if (ImGui.Button(HellionStrings.Settings_Integrations_GotAnIdea_LinkLabel))
|
||||
{
|
||||
Dalamud.Utility.Util.OpenLink(BrandingLinks.HellionForgeDiscordInvite);
|
||||
Plugin.PlatformUtil.OpenLink(BrandingLinks.HellionForgeDiscordInvite);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
{
|
||||
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||
Directory.CreateDirectory(dir);
|
||||
Dalamud.Utility.Util.OpenLink(dir);
|
||||
Plugin.PlatformUtil.OpenLink(dir);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -90,7 +90,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
var path = Path.Combine(dir, fileName);
|
||||
var json = ThemeJsonWriter.Serialize(active);
|
||||
File.WriteAllText(path, json);
|
||||
Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}");
|
||||
Plugin.LogProxy.Information($"Exported active theme '{active.Slug}' to {path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,6 +250,26 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
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.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
@@ -144,14 +144,20 @@ internal sealed class StatusBar
|
||||
ImGui.TextUnformatted(_cachedTellsText);
|
||||
}
|
||||
|
||||
// Slot 5: version, right-aligned, muted
|
||||
// Slot 5: version, right-aligned, muted. Hidden when the window is
|
||||
// too narrow to fit all five slots — the other four need ~200 px
|
||||
// before the version text starts clipping into them.
|
||||
var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
|
||||
var versionWidth = ImGui.CalcTextSize(versionText).X;
|
||||
var contentRegionMax = ImGui.GetContentRegionMax().X;
|
||||
ImGui.SameLine(contentRegionMax - versionWidth);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
||||
const float MinOtherSlotsWidth = 200f;
|
||||
if (contentRegionMax - versionWidth > MinOtherSlotsWidth)
|
||||
{
|
||||
ImGui.TextUnformatted(versionText);
|
||||
ImGui.SameLine(contentRegionMax - versionWidth);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
||||
{
|
||||
ImGui.TextUnformatted(versionText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ internal static class AutoTranslate
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
AllEntries();
|
||||
Plugin.Log.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
|
||||
Plugin.LogProxy.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
@@ -197,7 +197,7 @@ internal static class AutoTranslate
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, $"failed to translate: {lookup}");
|
||||
Plugin.LogProxy.Error(ex, $"failed to translate: {lookup}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
internal sealed class DalamudPlatformUtil : IPlatformUtil
|
||||
{
|
||||
public DalamudPlatformUtil()
|
||||
{
|
||||
// Util.IsWine probes the host process and never changes for the
|
||||
// lifetime of a plugin instance, so we cache it once at ctor.
|
||||
// Mirrors LightlessSync/Services/DalamudUtilService:154.
|
||||
IsWine = Dalamud.Utility.Util.IsWine();
|
||||
}
|
||||
|
||||
public bool IsWine { get; }
|
||||
|
||||
public void OpenLink(string url) => Dalamud.Utility.Util.OpenLink(url);
|
||||
}
|
||||
@@ -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,11 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
// Indirection over Dalamud.Utility.Util's static surface so services can be
|
||||
// constructed in an isolated xUnit AppDomain without loading Dalamud.dll.
|
||||
// Production wiring lives in DalamudPlatformUtil; tests substitute a fake.
|
||||
internal interface IPlatformUtil
|
||||
{
|
||||
bool IsWine { get; }
|
||||
|
||||
void OpenLink(string url);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
|
||||
namespace HellionChat.Util;
|
||||
|
||||
// Indirection over Dalamud's IPluginLog so MessageStore can be constructed
|
||||
// in an isolated xUnit AppDomain without loading Dalamud.dll — same pattern
|
||||
// as IPlatformUtil from F12.1. A later DI-container cycle (v1.5.x) may
|
||||
// replace this with Microsoft.Extensions.Logging's ILogger<T>.
|
||||
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);
|
||||
}
|
||||
@@ -254,6 +254,17 @@ internal static class ImGuiUtil
|
||||
return end;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Inspired by ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12).
|
||||
// Upstream dropped the width parameter (no callers there); we keep
|
||||
// it because two ChatLogWindow header buttons size themselves to
|
||||
// match the ChannelIcon button's frame. The actual bug is the
|
||||
// manual size = width - 2 * CellPadding.X subtraction: CellPadding
|
||||
// scales with HUD scale, the raw int does not, so the button
|
||||
// shrank under high HUD scales. ImGui.Button already handles its
|
||||
// own frame padding internally — pass the measured width straight
|
||||
// through.
|
||||
// ---------------------------------------------------------------
|
||||
internal static bool IconButton(
|
||||
FontAwesomeIcon icon,
|
||||
string? id = null,
|
||||
@@ -268,10 +279,7 @@ internal static class ImGuiUtil
|
||||
bool ret;
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
{
|
||||
var size = Vector2.Zero;
|
||||
if (width > 0)
|
||||
size.X = width - 2 * ImGui.GetStyle().CellPadding.X;
|
||||
|
||||
var size = width > 0 ? new Vector2(width, 0f) : Vector2.Zero;
|
||||
ret = ImGui.Button(label, size);
|
||||
}
|
||||
|
||||
@@ -575,7 +583,9 @@ internal static class ImGuiUtil
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,6 @@ public static class MemoryUtil
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
internal static class UrlValidation
|
||||
{
|
||||
// Used by BrandingLinks/IntegrationLinks at module init. A typo in a URL
|
||||
// rotation throws loudly at plugin load instead of silently failing when
|
||||
// a user clicks the broken button.
|
||||
public static void ValidateAll(string source, params string[] urls)
|
||||
{
|
||||
foreach (var url in urls)
|
||||
{
|
||||
if (
|
||||
!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme is not "https" and not "http")
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException($"{source} contains malformed URL: {url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ public static class WrapperUtil
|
||||
{
|
||||
try
|
||||
{
|
||||
Plugin.Log.Debug($"Opening URI {uri} in default browser");
|
||||
Dalamud.Utility.Util.OpenLink(uri.ToString());
|
||||
Plugin.LogProxy.Debug($"Opening URI {uri} in default browser");
|
||||
Plugin.PlatformUtil.OpenLink(uri.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error($"Error opening URI: {ex}");
|
||||
Plugin.LogProxy.Error($"Error opening URI: {ex}");
|
||||
AddNotification(Language.Context_OpenInBrowserError, NotificationType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
+107
-107
@@ -1,110 +1,110 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[15.0.0, )",
|
||||
"resolved": "15.0.0",
|
||||
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
||||
},
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.4, 4.0.0)",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "3.1.4",
|
||||
"MessagePackAnalyzer": "3.1.4",
|
||||
"Microsoft.NET.StringTools": "17.11.4"
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[15.0.0, )",
|
||||
"resolved": "15.0.0",
|
||||
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
||||
},
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.4, 4.0.0)",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "3.1.4",
|
||||
"MessagePackAnalyzer": "3.1.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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://github.com/goatcorp/Dalamud)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://www.finalfantasyxiv.com/)
|
||||
@@ -11,7 +11,7 @@
|
||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||
</p>
|
||||
|
||||
**Version 1.4.4** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||
**Version 1.4.7** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||
|
||||
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
|
||||
@@ -102,7 +102,7 @@ Hellion Chat is developed under **Hellion Forge**, the specialized modding and p
|
||||
#### Custom Themes (v1.1.0)
|
||||
|
||||
HellionChat ships a theme engine with ten built-in themes (Hellion Arctic, Hellion Spectrum, Chat 2 Classic, Event
|
||||
Horizon, Moonlit Bloom, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman, Synthwave Sunset) and a JSON-based
|
||||
Horizon, Crystal Nocturne, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman, Synthwave Sunset) and a JSON-based
|
||||
authoring format for custom themes. Schema and step-by-step guide in
|
||||
[`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md). Hellion Spectrum is Deuteranopia/Protanopia-safe (red-green color
|
||||
blindness) based on the Wong/Okabe-Ito palette.
|
||||
@@ -286,16 +286,23 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
|
||||
|
||||
## Project Status
|
||||
|
||||
**Version 1.4.4** — Threading and IPC safety polish on top of the v1.4.3 async-load foundation. The
|
||||
`AutoTellTabsService` hot-path getter now reads from an `Interlocked` counter instead of taking a lock on every
|
||||
render frame, with a resync hook for the snapshot-restore path in `SaveConfig` and a pure-helper test mirror in the
|
||||
Build-Suite repo. The Honorific integration carries per-method threading banners so the framework-thread invariant is
|
||||
visible at the call site, and an unsubscribe failure now logs at Warning instead of Debug — a leaked subscription
|
||||
across plugin reloads is exactly the kind of thing that should not be silent. The AutoTranslate warmup thread is
|
||||
finally marked `IsBackground = true`, matching the pattern used in `MessageManager` and `Plugin.RetentionSweep` since
|
||||
v1.4.0. The privacy filter logs once per unknown ChatType so a future patch's added channel does not drop off the
|
||||
radar, and new installs default `PrivacyPersistUnknownChannels` to `true` as a failsafe; existing configs keep their
|
||||
explicit choice. No schema bump, no migration. Fifth sub-patch of the v1.4.x polish sweep series (as of 2026-05-12).
|
||||
**Version 1.4.7** — Backlog cleanup and the first user-visible feature bundle since v1.4.5. TempTell tabs can now be
|
||||
pinned via right-click; pinned tabs survive relog, keep their conversation history (loaded on demand from the message
|
||||
store), and stay bound to the same `/tell` partner. A hard cap of 5 pinned tabs lives in a pool separate from the 15-tab
|
||||
auto-tell pool, so the total ceiling is 20 tabs. The sidebar groups pinned tabs into their own section with its own
|
||||
divider header. Honorific glow outlines now render when the title carries a Glow colour — opt-in via **Settings →
|
||||
Integrations → Render glow outlines (Honorific)**, default off, so v1.4.6 visuals stay untouched for users who don't
|
||||
care and the per-frame DrawList overhead is skipped on low-end hardware. Honorific gradient (Color3 / GradientColourSet
|
||||
/ Wave / Pulse) is parsed and stashed for a later cycle, but currently renders as the primary colour. Sidebar width is
|
||||
configurable in **Theme & Layout** between 44 and 160 px; default stays icon-only so existing users see no layout
|
||||
change. `Configuration.UpdateFrom` now preserves the runtime `CurrentChannel` across the persistent-tab merge, and
|
||||
`TabSwitched` deep-clones the seeded channel — together they fix a Settings-Save regression where the chat input could
|
||||
pop back to `/tell <pinned-partner>` after touching settings while on a Party or Linkshell tab. Internal items:
|
||||
`IPluginLogProxy` indirection over Dalamud's `IPluginLog` routes all ~91 `Plugin.Log` call sites through a testable
|
||||
proxy, closing the test-isolation gap F12.1 left in v1.4.6 (`MessageStore.Migrate0` now runs in xUnit without loading
|
||||
`Dalamud.dll`). `Util/ImGuiUtil.cs`'s `DrawArrows` IconButton id gets explicit parentheses on the increment. Migration
|
||||
v16 → v17 is additive (new `Tab.IsPinned` flag, default false). Eighth sub-patch of the v1.4.x polish sweep series (as
|
||||
of 2026-05-13).
|
||||
|
||||
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
||||
|
||||
|
||||
+127
-11
@@ -10,6 +10,122 @@ to the release pages for details.
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two
|
||||
upstream-inherited bugs from ChatTwo `f35b7d3`, and prepares the code for the v1.4.7 backlog cleanup.
|
||||
|
||||
- `scripts/preflight.sh` gains Block E (`dotnet csharpier check`) and Block F (`markdownlint-cli2`) so reflow drift and
|
||||
markdown violations are caught at the pre-push gate. `.markdownlint.json` adds `MD024 siblings_only` and disables
|
||||
`MD036` so the bilingual forge-post bold-emphasis headings pass linting; the `.claude/` directory is excluded from the
|
||||
scan
|
||||
- `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
|
||||
atlas-toolkit throw triggered the NotoSansCjkRegular fallback
|
||||
- `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
|
||||
`InvalidOperationException` at plugin load with the source class and the broken URL in the message
|
||||
- Cherry-picked from ChatTwo upstream `f35b7d3`: `Chat.SetChannel` no longer leaks the native `Utf8String` when the
|
||||
linkshell check rejects the channel. The validity check is now wrapped around the `ChangeChatChannel` call instead of
|
||||
short-circuiting before `Dtor`. `ValidAnyLinkshell` is renamed to `IsChannelOrExistingLinkshell` and the
|
||||
`ChatLogWindow` call-site follows the rename
|
||||
- Cherry-picked from ChatTwo upstream `f35b7d3`: `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget`. The old
|
||||
`CurrentChannel = CurrentChannel` was a reference copy, so PopOut and Temp tabs mutated each other's channel state
|
||||
(incl. tell target). `TellTarget.From(t)` static factory is replaced with an instance `Clone()`; `UsedChannel.Clone()`
|
||||
is new and runs deep-clone on both TellTarget references
|
||||
- `ChatLogWindow` active-tab underline pill now scales with `ImGuiHelpers.GlobalScale` and rounds its DrawList
|
||||
coordinates to physical pixels via `MathF.Round`, so the 2 px line stays crisp on 125 % and 150 % DPI setups instead
|
||||
of bleeding into a sub-pixel blur
|
||||
- `ImGuiUtil.IconButton` width parameter no longer subtracts HUD-scaled `CellPadding.X * 2` from the raw `int` width.
|
||||
`ImGui.Button` handles its own frame padding internally, so the measured `buttonWidth` now passes through verbatim
|
||||
(inspired-by upstream `f35b7d3`, but our two call-sites need the parameter, so the param itself stays)
|
||||
- Internal: `HellionStyle` ChildBgAlpha threshold logic extracted to `HellionStyleHelpers.ResolveChildBgAlpha` with a
|
||||
build-suite mirror test that pins the 0.999f cutoff. `Plugin.SaveConfig` clones only the temp-tab subset in the
|
||||
pre-serialization snapshot instead of the full tab list. `SettingsOverview` caches `ImGui.GetWindowDrawList()` once
|
||||
per frame and passes the pointer down to `DrawCard`
|
||||
- Internal: `Dalamud.Utility.Util` static surface (`IsWine`, `OpenLink`) routed through a new `IPlatformUtil`
|
||||
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
|
||||
Dalamud-static surface, slated for v1.4.7)
|
||||
- Built-in themes: Crystal Nocturne (royal sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit
|
||||
Bloom in the built-in roster. Users who had Moonlit Bloom selected fall back to the default Hellion Arctic on the
|
||||
first plugin load; an existing custom JSON copy of Moonlit Bloom under `pluginConfigs/HellionChat/themes/` keeps
|
||||
working unchanged
|
||||
|
||||
Modding & support: join Hellion Forge — <https://discord.gg/X9V7Kcv5gR>
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.4.5 — UX and Robustness (2026-05-12)
|
||||
|
||||
Sixth sub-patch of the v1.4.x polish-sweep series. User-visible robustness fixes plus two doc/test polish items from the
|
||||
audit backlog. No schema bump, no migration.
|
||||
|
||||
- `ChatLogWindow.Draw` now surfaces a one-shot warning notification when the draw path throws. The stack trace still
|
||||
goes to `/xllog` via `Plugin.Log.Error`; the notification is suppressed for the rest of the plugin session so a
|
||||
recurring failure can't spam the notification stack frame-by-frame. Pattern-match to the existing `Plugin.cs:505-516`
|
||||
migration-blocker notification
|
||||
- `FirstRunWizard` splits accept from close. `OnClose` no longer silently sets `FirstRunCompleted`, so closing the X
|
||||
leaves the wizard pending and it reopens on the next plugin load. A new footer "Later — keep defaults" button is the
|
||||
explicit path to dismiss without picking a profile. Bilingual strings (EN + DE) plus a tooltip
|
||||
- `InputHistoryService.Reset` is wired into `Plugin.DisposeAsync` alongside the existing pure-memory cleanups. Static
|
||||
state used to survive a plugin reload — the next load now starts with an empty history
|
||||
- `FontManager.GetHellionFontBytes` becomes `TryGetHellionFontBytes` with a nullable return. On miss (broken csproj,
|
||||
hand-rolled dev build) the caller falls back to the system-font path that `UseHellionFont=false` already uses, plus a
|
||||
`Plugin.Log.Warning`. The whole UiBuilder no longer throws if the embedded font resource is absent
|
||||
- `Plugin.cs:167-168` gets a 4-line reasoning comment around the session-only `RemoveAll(IsTempTab)`: tells are usually
|
||||
privacy-filtered, resurrecting an empty crashed-session tab would trigger DB reconstruction on the next load.
|
||||
`TempTabCounter.InitFromList` mirrors the post-strip semantic in the Build-Suite with a pinning test
|
||||
- `StatusBar.cs` drops the version slot when the chat window's content width minus the version text is below 200 px. The
|
||||
right-aligned version used to clip into the four left-side slots in narrow windows
|
||||
|
||||
Modding & support: join Hellion Forge — <https://discord.gg/X9V7Kcv5gR>
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.4.4 — Threading and IPC Safety Polish (2026-05-12)
|
||||
|
||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock
|
||||
@@ -20,21 +136,21 @@ unknown ChatType shows up.
|
||||
in sync with `Config.Tabs` from inside the existing mutation paths. `Initialize()` seeds the counter from the
|
||||
persisted Tabs list, and `SaveConfig`'s snapshot-restore path calls a new `ResyncTempTabCounter()` so the mid-step
|
||||
`RemoveAll` doesn't leave the counter drifting. Pure-helper test mirror lives in the Build-Suite repo
|
||||
- `HonorificService` per-method threading banners replace the block comment at the bottom of the file. Each IPC
|
||||
callback (`TryInitialPull`, `OnTitleChanged`, `OnReady`, `OnDisposing`, `TryUnsubscribe`) and the `CurrentTitle`
|
||||
field carry a one-line `// Thread:` annotation so the framework-thread invariant is visible at the call site
|
||||
- `HonorificService` per-method threading banners replace the block comment at the bottom of the file. Each IPC callback
|
||||
(`TryInitialPull`, `OnTitleChanged`, `OnReady`, `OnDisposing`, `TryUnsubscribe`) and the `CurrentTitle` field carry a
|
||||
one-line `// Thread:` annotation so the framework-thread invariant is visible at the call site
|
||||
- `TryUnsubscribe` log-level upgraded from `Debug` to `Warning`. A silent unsubscribe failure leaks a live subscription
|
||||
across plugin reloads, which is exactly the kind of issue that should not be at Debug
|
||||
- `AutoTranslate.PreloadCache` thread now has `IsBackground = true` and a thread name. Without `IsBackground` the
|
||||
warmup blocks plugin unload (typically 100-300 ms). Pattern-match to `MessageManager` (F6.1) and `Plugin.RetentionSweep`
|
||||
- `AutoTranslate.PreloadCache` thread now has `IsBackground = true` and a thread name. Without `IsBackground` the warmup
|
||||
blocks plugin unload (typically 100-300 ms). Pattern-match to `MessageManager` (F6.1) and `Plugin.RetentionSweep`
|
||||
(F9.3), both since v1.4.0
|
||||
- `Configuration.IsAllowedForStorage` adds a one-line `Plugin.Log.Warning` for the first occurrence of any ChatType
|
||||
that isn't in `PrivacyPersistChannels`. Dedup via a `NonSerialized` `HashSet<ChatType>`, so the warning fires once
|
||||
per runtime — not once per frame, not once per install. Failsafe routing through `PrivacyPersistUnknownChannels`
|
||||
is unchanged
|
||||
- `Configuration.IsAllowedForStorage` adds a one-line `Plugin.Log.Warning` for the first occurrence of any ChatType that
|
||||
isn't in `PrivacyPersistChannels`. Dedup via a `NonSerialized` `HashSet<ChatType>`, so the warning fires once per
|
||||
runtime — not once per frame, not once per install. Failsafe routing through `PrivacyPersistUnknownChannels` is
|
||||
unchanged
|
||||
- `PrivacyPersistUnknownChannels` field default flipped from `false` to `true` for new installs via a constant in
|
||||
`PrivacyDefaults`. Existing configs keep their explicit choice — the deserializer overrides the initializer. No
|
||||
schema bump, no migration, no first-run banner
|
||||
`PrivacyDefaults`. Existing configs keep their explicit choice — the deserializer overrides the initializer. No schema
|
||||
bump, no migration, no first-run banner
|
||||
|
||||
Modding & support: join Hellion Forge — <https://discord.gg/X9V7Kcv5gR>
|
||||
|
||||
|
||||
+66
-4
@@ -10,14 +10,76 @@ the plugin's privacy-first scope during brainstorming.
|
||||
|
||||
---
|
||||
|
||||
## Next Cycle (v1.4.4)
|
||||
## Next Cycle (v1.4.8)
|
||||
|
||||
**Window-Lazy-Open + Render-Init-Cost Optimisation** — take the `IAsyncDalamudPlugin` foundation laid in v1.4.3 and turn
|
||||
it into wins users can actually feel. Window construction deferred until first open, render-path init cost reduced in
|
||||
the first frames. Concrete candidates and size estimates will be consolidated in the v1.4.4 brainstorm.
|
||||
**Hook-Layer Cycle.** Receive-suppressed-tells toggle (cross-reference XIVIM #73 bubble-layer sub-task), Database Viewer
|
||||
full-text search via SQLite FTS5, plus preparation for the later Ad-Block cycle. Hook-layer investigation is shared
|
||||
across these items so they cluster naturally in one sub-patch.
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
Seventh sub-patch of the v1.4.x Polish Sweep series. Maintenance patch — no user-visible behaviour changes; tightens the
|
||||
development feedback loop and pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier reflow
|
||||
check (Block E) and a markdownlint pass (Block F), so style drift and markdown violations are blocked at the pre-push
|
||||
gate. `FontManager.AddFontWithFallback` catch-filter now spans `InvalidOperationException` and `ArgumentException` on
|
||||
top of the existing IO triad, with the exception type name in the warning log so the diagnostic path can see which
|
||||
atlas-toolkit throw triggered the fallback. `BrandingLinks` and `IntegrationLinks` run a `[ModuleInitializer]` URL
|
||||
validation pass on plugin load; a typo in a future URL rotation now throws at startup instead of failing silently when a
|
||||
user clicks the broken button. Cherry-picked from ChatTwo upstream `f35b7d3`: `Chat.SetChannel` no longer leaks the
|
||||
native `Utf8String` when the linkshell check rejects the channel (rename to `IsChannelOrExistingLinkshell` plus
|
||||
wrap-not-return), and `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget` (the previous reference copy let PopOut
|
||||
and Temp tabs mutate each other's channel state). The `ChatLogWindow` active-tab underline pill scales with
|
||||
`ImGuiHelpers.GlobalScale` and rounds to physical pixels for crisp rendering above 100 % DPI. Internal items:
|
||||
`HellionStyle` ChildBgAlpha extracted to a testable helper, `Plugin.SaveConfig` clones only the temp-tab subset in the
|
||||
snapshot path, `SettingsOverview` caches the draw-list per frame, `Dalamud.Utility.Util` static surface routed through
|
||||
an `IPlatformUtil` indirection (`MessageStore`'s `IsWine` probe is now testable in isolation). No schema bump, no
|
||||
migration.
|
||||
|
||||
## v1.4.5 — UX and Robustness (released 2026-05-12)
|
||||
|
||||
Sixth sub-patch of the v1.4.x Polish Sweep series. User-visible robustness polish plus two doc/test polish items from
|
||||
the audit backlog. Chat-log draw failures now surface as a one-shot notification instead of failing silently. The
|
||||
first-run wizard splits accept from close: `OnClose` no longer silently sets `FirstRunCompleted`, and a new footer
|
||||
"Later — keep defaults" button is the explicit path to dismiss without picking a profile. `InputHistoryService` clears
|
||||
on plugin dispose so the previous session's typed commands don't bleed into the next load. `FontManager` falls back to
|
||||
the system font path if the embedded Hellion font resource is missing (broken-csproj / dev-build only). The status bar
|
||||
hides the version slot when the chat window is too narrow to fit all five slots without overlap. Plus
|
||||
`Plugin.cs:167-168` gains an explicit session-only Auto-Tell-Tab invariant comment with a `TempTabCounter.InitFromList`
|
||||
pin in the Build-Suite. No schema bump, no migration.
|
||||
|
||||
## v1.4.4 — Threading and IPC Safety Polish (released 2026-05-12)
|
||||
|
||||
Fifth sub-patch of the v1.4.x Polish Sweep series. `AutoTellTabsService.ActiveTempTabCount` switches from a
|
||||
lock-protected LINQ `Count` to an `Interlocked` counter kept in sync from inside the existing mutation paths;
|
||||
`Initialize()` seeds from the persisted Tabs list and `SaveConfig`'s snapshot-restore path calls a new
|
||||
`ResyncTempTabCounter()` after the mid-step `RemoveAll`. `HonorificService` carries per-method threading banners and
|
||||
`TryUnsubscribe`'s log level moves from Debug to Warning. `AutoTranslate.PreloadCache` is marked `IsBackground = true`
|
||||
so plugin unload no longer waits for it. `Configuration.IsAllowedForStorage` logs once per unknown ChatType via a
|
||||
`NonSerialized` `HashSet`, and `PrivacyPersistUnknownChannels` default flips to `true` for new installs. No schema bump,
|
||||
no migration.
|
||||
|
||||
## v1.4.3 — Plugin-Load Async-Init + Repo-Cutover (released 2026-05-08)
|
||||
|
||||
Fourth and largest sub-patch of the v1.4.x Polish Sweep series. Plugin migrated to Dalamud's `IAsyncDalamudPlugin` API:
|
||||
|
||||
@@ -141,7 +141,7 @@ A theme can tint these toward its brand family (e.g., a purple theme can shift T
|
||||
**don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual
|
||||
hierarchy.
|
||||
|
||||
The eight colored built-in themes (Hellion Arctic, Hellion Spectrum, Event Horizon, Moonlit Bloom, Mint Grove, Night
|
||||
The eight colored built-in themes (Hellion Arctic, Hellion Spectrum, Event Horizon, Crystal Nocturne, Mint Grove, Night
|
||||
Blue, Indigo Violet, Forge Merchantman) all follow this rule — read their source for reference. Chat 2 Klassik
|
||||
intentionally ships without `chatChannels` so the user keeps their existing picks.
|
||||
|
||||
|
||||
+12
-2
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# preflight.sh — pre-push gate. Blocks A/B/C verify config drift; Block D is a
|
||||
# headless `dotnet build` to catch compile-time API drift. Test execution lives
|
||||
# in the local Build-Suite repo and is NOT part of this preflight.
|
||||
# headless `dotnet build` to catch compile-time API drift; Block E runs
|
||||
# `dotnet csharpier check` against HellionChat/; Block F runs markdownlint
|
||||
# against the repo's *.md files. Test execution lives in the local Build-Suite
|
||||
# repo and is NOT part of this preflight.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
@@ -19,4 +21,12 @@ echo "==> preflight: Block C — changelog sync"
|
||||
echo "==> preflight: Block D — plugin compile health"
|
||||
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
|
||||
|
||||
echo "==> preflight: Block E — csharpier reflow check"
|
||||
dotnet csharpier check HellionChat/
|
||||
|
||||
echo "==> preflight: Block F — markdownlint"
|
||||
# npx --yes avoids a global install; first run caches into ~/.npm/_npx/.
|
||||
# Subsequent runs are sub-second.
|
||||
npx --yes markdownlint-cli2 "**/*.md" "#node_modules" "#bin" "#obj" "#.claude"
|
||||
|
||||
echo "==> preflight: ALL GREEN"
|
||||
|
||||
Reference in New Issue
Block a user