Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
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
|
(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
|
- **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
|
geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription kann den Service über Plugin-Reloads
|
||||||
Plugin-Reloads hinweg leben lassen, also läuft der Log jetzt auf Warning
|
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`
|
- **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
|
unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu MessageManager und RetentionSweep (beide
|
||||||
(beide seit v1.4.0)
|
seit v1.4.0)
|
||||||
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType einführt der weder
|
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType einführt der weder in
|
||||||
in der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der
|
der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der Filter
|
||||||
Filter einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet,
|
einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet, also kein
|
||||||
also kein Log-Spam
|
Log-Spam
|
||||||
- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen Configs jetzt auf `true`,
|
- **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.
|
damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt wird bevor der User entscheiden kann. Bestehende
|
||||||
Bestehende Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein
|
Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein Schema-Bump
|
||||||
Schema-Bump
|
|
||||||
|
|
||||||
Keine User-sichtbaren Funktions-Änderungen außer dem Default-Flip für neue Installationen. Settings, Themes, Tabs und
|
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.
|
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,18 @@
|
|||||||
|
---
|
||||||
|
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).
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"MD007": { "indent": 4 },
|
"MD007": { "indent": 4 },
|
||||||
"MD013": false,
|
"MD013": false,
|
||||||
|
"MD024": { "siblings_only": true },
|
||||||
"MD029": false,
|
"MD029": false,
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
|
"MD036": false,
|
||||||
"MD041": false
|
"MD041": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Branding;
|
namespace HellionChat.Branding;
|
||||||
|
|
||||||
// Centralised — a future invite/URL rotation only touches this file.
|
// 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";
|
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
||||||
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
||||||
public const string HellionMediaWebsite = "https://hellion-media.de/de";
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ public class Tab
|
|||||||
Opacity = Opacity,
|
Opacity = Opacity,
|
||||||
Identifier = Identifier,
|
Identifier = Identifier,
|
||||||
InputDisabled = InputDisabled,
|
InputDisabled = InputDisabled,
|
||||||
CurrentChannel = CurrentChannel,
|
CurrentChannel = CurrentChannel.Clone(),
|
||||||
CanMove = CanMove,
|
CanMove = CanMove,
|
||||||
CanResize = CanResize,
|
CanResize = CanResize,
|
||||||
IndependentHide = IndependentHide,
|
IndependentHide = IndependentHide,
|
||||||
@@ -512,7 +512,7 @@ public class Tab
|
|||||||
HideWhenInactive = HideWhenInactive,
|
HideWhenInactive = HideWhenInactive,
|
||||||
IsTempTab = IsTempTab,
|
IsTempTab = IsTempTab,
|
||||||
AllSenderMessages = AllSenderMessages,
|
AllSenderMessages = AllSenderMessages,
|
||||||
TellTarget = TellTarget.From(TellTarget),
|
TellTarget = TellTarget.Clone(),
|
||||||
IsGreeted = IsGreeted,
|
IsGreeted = IsGreeted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -690,6 +690,29 @@ public class UsedChannel
|
|||||||
{
|
{
|
||||||
Channel = channel;
|
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]
|
[Serializable]
|
||||||
|
|||||||
@@ -44,16 +44,26 @@ public class FontManager
|
|||||||
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
||||||
private static byte[]? HellionFontBytes;
|
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)
|
if (HellionFontBytes is not null)
|
||||||
return HellionFontBytes;
|
return HellionFontBytes;
|
||||||
|
|
||||||
using var stream =
|
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
|
||||||
typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
|
"HellionFont.ttf"
|
||||||
?? throw new FileNotFoundException(
|
|
||||||
"Hellion font resource not embedded in the assembly"
|
|
||||||
);
|
);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
Plugin.Log.Warning(
|
||||||
|
"Hellion font resource missing — falling back to system default font."
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
stream.CopyTo(ms);
|
stream.CopyTo(ms);
|
||||||
HellionFontBytes = ms.ToArray();
|
HellionFontBytes = ms.ToArray();
|
||||||
@@ -146,8 +156,11 @@ public class FontManager
|
|||||||
? Plugin.Config.FontSizeV2
|
? Plugin.Config.FontSizeV2
|
||||||
: Plugin.Config.GlobalFontV2.SizePt;
|
: Plugin.Config.GlobalFontV2.SizePt;
|
||||||
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
||||||
config.MergeFont = Plugin.Config.UseHellionFont
|
// F10.2: if the embedded font is missing, drop to the system font
|
||||||
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
|
// 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");
|
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||||
|
|
||||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||||
@@ -213,11 +226,21 @@ public class FontManager
|
|||||||
return fontId.AddToBuildToolkit(tk, config);
|
return fontId.AddToBuildToolkit(tk, config);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
|
when (e
|
||||||
|
is FileNotFoundException
|
||||||
|
or DirectoryNotFoundException
|
||||||
|
or IOException
|
||||||
|
or InvalidOperationException
|
||||||
|
or ArgumentException
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
// 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.Log.Warning(
|
Plugin.Log.Warning(
|
||||||
e,
|
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);
|
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
|
||||||
return fallback.AddToBuildToolkit(tk, config);
|
return fallback.AddToBuildToolkit(tk, config);
|
||||||
|
|||||||
@@ -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();
|
var idx = channel.LinkshellIndex();
|
||||||
if (idx == uint.MaxValue || channel.IsExtraChatLinkshell())
|
if (idx == uint.MaxValue || channel.IsExtraChatLinkshell())
|
||||||
return true;
|
return true;
|
||||||
if (channel.IsLinkshell() && ValidLinkshell(idx))
|
|
||||||
return true;
|
if (channel.IsLinkshell())
|
||||||
if (channel.IsCrossLinkshell() && ValidCrossLinkshell(idx))
|
return ValidLinkshell(idx);
|
||||||
return true;
|
|
||||||
|
if (channel.IsCrossLinkshell())
|
||||||
|
return ValidCrossLinkshell(idx);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,12 +539,17 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
if (idx == uint.MaxValue)
|
if (idx == uint.MaxValue)
|
||||||
idx = 0;
|
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
|
RaptureShellModule
|
||||||
.Instance()
|
.Instance()
|
||||||
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||||
|
|
||||||
target->Dtor(true);
|
target->Dtor(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,5 +40,11 @@ public class TellTarget
|
|||||||
|
|
||||||
public static TellTarget Empty() => new(string.Empty, 0, 0, TellReason.Direct);
|
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">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
<Version>1.4.4</Version>
|
<Version>1.4.6</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Use lock file to pin exact versions -->
|
<!-- Use lock file to pin exact versions -->
|
||||||
|
|||||||
@@ -35,6 +35,68 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
|
**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**
|
||||||
|
|
||||||
|
Maintenance patch. No user-visible behaviour changes; tightens the
|
||||||
|
development feedback loop, fixes two upstream-inherited bugs, and
|
||||||
|
prepares the code for the v1.4.7 backlog cleanup.
|
||||||
|
|
||||||
|
- preflight.sh gains a csharpier reflow check and a markdownlint
|
||||||
|
pass so style drift and markdown violations are caught at the
|
||||||
|
pre-push gate
|
||||||
|
- FontManager fallback catches the full set of atlas-toolkit
|
||||||
|
throws (IO, InvalidOperation, ArgumentException) — a corrupt
|
||||||
|
font config no longer takes down the whole atlas build
|
||||||
|
- BrandingLinks and IntegrationLinks URLs validated on plugin
|
||||||
|
load — a typo in a future URL rotation now throws at startup
|
||||||
|
- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel
|
||||||
|
no longer leaks the native Utf8String when the linkshell check
|
||||||
|
rejects the channel
|
||||||
|
- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now
|
||||||
|
deep-clones UsedChannel and TellTarget — PopOut and Temp tabs
|
||||||
|
no longer mutate each other's channel state
|
||||||
|
- Active-tab underline scales with DPI and rounds to physical
|
||||||
|
pixels for crisp rendering above 100% scaling
|
||||||
|
- IconButton width parameter no longer subtracts HUD-scaled
|
||||||
|
padding from a raw int (measured width passes through verbatim)
|
||||||
|
- Internal: HellionStyle ChildBgAlpha extracted to a testable
|
||||||
|
helper; Plugin.SaveConfig clones only the temp tabs;
|
||||||
|
SettingsOverview caches the draw-list per frame;
|
||||||
|
Dalamud.Utility.Util surface routed through an IPlatformUtil
|
||||||
|
indirection (MessageStore IsWine probe is now testable in
|
||||||
|
isolation)
|
||||||
|
- Built-in themes: Crystal Nocturne (sapphire and electric
|
||||||
|
magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom.
|
||||||
|
Users with Moonlit Bloom selected fall back to Hellion Arctic
|
||||||
|
on first load
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**v1.4.5 — UX and Robustness (2026-05-12)**
|
||||||
|
|
||||||
|
Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw
|
||||||
|
failures surface as a notification, the first-run wizard has an
|
||||||
|
explicit "Later" option, the input history clears on plugin reload,
|
||||||
|
and the status bar version slot stops clipping in narrow windows.
|
||||||
|
|
||||||
|
- Chat window draw errors now show a one-shot notification instead
|
||||||
|
of failing silently — stack trace stays in /xllog
|
||||||
|
- First-run wizard: explicit "Later — keep defaults" button.
|
||||||
|
Closing the X no longer silently accepts the defaults; the wizard
|
||||||
|
reopens on the next plugin load if nothing was picked
|
||||||
|
- InputHistoryService clears on plugin dispose so the previous
|
||||||
|
session's typed commands don't bleed into the next load
|
||||||
|
- Status bar hides the version slot when the chat window is too
|
||||||
|
narrow to fit all five slots without overlap
|
||||||
|
- Internal: explicit session-only Auto-Tell-Tab invariant in
|
||||||
|
Plugin.cs plus a pinning test in the Build-Suite
|
||||||
|
- Internal: FontManager falls back to the system font if the
|
||||||
|
embedded Hellion font resource is missing — logs a Warning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**v1.4.4 — Threading and IPC safety polish (2026-05-12)**
|
**v1.4.4 — Threading and IPC safety polish (2026-05-12)**
|
||||||
|
|
||||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
|
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
|
||||||
@@ -70,15 +132,4 @@ changelog: |-
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**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
|
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).
|
// Shared input history for all ChatInputBars (main and pop-out windows).
|
||||||
// Push deduplicates: existing entries are moved to the end when re-added.
|
// Push deduplicates: existing entries are moved to the end when re-added.
|
||||||
|
// TEST-MIRROR: ../../Hellion Build test/Util/InputHistoryServiceTests.cs
|
||||||
public static class InputHistoryService
|
public static class InputHistoryService
|
||||||
{
|
{
|
||||||
private const int MaxSize = 30;
|
private const int MaxSize = 30;
|
||||||
@@ -41,4 +42,12 @@ public static class InputHistoryService
|
|||||||
return null;
|
return null;
|
||||||
return _entries[cursor];
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Integrations;
|
namespace HellionChat.Integrations;
|
||||||
|
|
||||||
// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
|
// 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 HonorificRepo = "https://github.com/Caraxi/Honorific";
|
||||||
public const string HonorificAuthor = "https://github.com/Caraxi";
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
|
||||||
Store = new MessageStore(DatabasePath());
|
Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil);
|
||||||
|
|
||||||
PendingMessageThread = new Thread(() =>
|
PendingMessageThread = new Thread(() =>
|
||||||
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using MessagePack;
|
|||||||
using MessagePack.Formatters;
|
using MessagePack.Formatters;
|
||||||
using MessagePack.Resolvers;
|
using MessagePack.Resolvers;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using DalamudUtil = Dalamud.Utility.Util;
|
|
||||||
using Encoding = System.Text.Encoding;
|
using Encoding = System.Text.Encoding;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
@@ -137,9 +136,12 @@ internal class MessageStore : IDisposable
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
internal MessageStore(string dbPath)
|
private readonly IPlatformUtil _platformUtil;
|
||||||
|
|
||||||
|
internal MessageStore(string dbPath, IPlatformUtil platformUtil)
|
||||||
{
|
{
|
||||||
DbPath = dbPath;
|
DbPath = dbPath;
|
||||||
|
_platformUtil = platformUtil;
|
||||||
Connection = Connect();
|
Connection = Connect();
|
||||||
Migrate();
|
Migrate();
|
||||||
}
|
}
|
||||||
@@ -166,7 +168,7 @@ internal class MessageStore : IDisposable
|
|||||||
conn.Open();
|
conn.Open();
|
||||||
conn.Execute(@"PRAGMA journal_mode=WAL;");
|
conn.Execute(@"PRAGMA journal_mode=WAL;");
|
||||||
conn.Execute(@"PRAGMA synchronous=NORMAL;");
|
conn.Execute(@"PRAGMA synchronous=NORMAL;");
|
||||||
if (DalamudUtil.IsWine())
|
if (_platformUtil.IsWine)
|
||||||
conn.Execute(@"PRAGMA cache_size = 32768;");
|
conn.Execute(@"PRAGMA cache_size = 32768;");
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-10
@@ -113,6 +113,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||||
internal Integrations.HonorificService HonorificService { 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!;
|
||||||
|
|
||||||
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||||
private int _disposeStarted;
|
private int _disposeStarted;
|
||||||
|
|
||||||
@@ -154,17 +158,24 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||||
|
|
||||||
|
// Wire platform indirection before LoadAsync allocates anything that
|
||||||
|
// needs Util.* — services then read Plugin.PlatformUtil instead of
|
||||||
|
// hitting the Dalamud static surface directly.
|
||||||
|
PlatformUtil = new DalamudPlatformUtil();
|
||||||
|
|
||||||
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
||||||
// must install v1.4.2 first to run the migration chain.
|
// must install v1.4.2 first to run the migration chain.
|
||||||
if (Config.Version < 16)
|
if (Config.Version < 16)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"HellionChat v1.4.4 requires config schema v16, got v{Config.Version}. "
|
$"HellionChat v1.4.6 requires config schema v16, got v{Config.Version}. "
|
||||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.4."
|
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.6."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop session-only Auto-Tell-Tabs that a previous crash may have persisted.
|
// Session-only tabs are stripped on every load; AutoTellTabsService.Initialize
|
||||||
|
// then re-pegs TempTabCounter from the stripped list, not the pre-strip snapshot.
|
||||||
|
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||||
|
|
||||||
LanguageChanged(Interface.UiLanguage);
|
LanguageChanged(Interface.UiLanguage);
|
||||||
@@ -372,6 +383,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
||||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
||||||
failure = CaptureFailure(failure, () => EmoteCache.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)
|
if (failure is not null)
|
||||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||||
@@ -633,18 +646,20 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
internal void SaveConfig()
|
internal void SaveConfig()
|
||||||
{
|
{
|
||||||
// Strip session-only Auto-Tell-Tabs before serialization; restore after.
|
// Session-only Auto-Tell-Tabs aren't persisted, so they move aside
|
||||||
var snapshot = Config.Tabs.ToList();
|
// before serialization and re-attach after. Cloning only the temp
|
||||||
|
// subset keeps the allocation proportional to AutoTellTabsLimit
|
||||||
|
// (<=15) instead of the full tab list.
|
||||||
|
var tempTabs = Config.Tabs.Where(t => t.IsTempTab).ToList();
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||||
|
|
||||||
Interface.SavePluginConfig(Config);
|
Interface.SavePluginConfig(Config);
|
||||||
|
|
||||||
Config.Tabs.Clear();
|
Config.Tabs.AddRange(tempTabs);
|
||||||
Config.Tabs.AddRange(snapshot);
|
|
||||||
|
|
||||||
// F2.1: snapshot-restore preserves IsTempTab tabs but the mid-step
|
// F2.1: the mid-step RemoveAll bypasses AutoTellTabsService, so
|
||||||
// RemoveAll bypasses AutoTellTabsService, so re-peg the counter.
|
// re-peg the counter. Null-conditional because SaveConfig can fire
|
||||||
// Null-conditional because SaveConfig can fire before Phase-2 init.
|
// before Phase-2 init.
|
||||||
AutoTellTabsService?.ResyncTempTabCounter();
|
AutoTellTabsService?.ResyncTempTabCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
|
||||||
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
|
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_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_Heading => Get(nameof(Export_Heading));
|
||||||
internal static string Export_Help => Get(nameof(Export_Help));
|
internal static string Export_Help => Get(nameof(Export_Help));
|
||||||
|
|||||||
@@ -222,6 +222,12 @@
|
|||||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||||
<value>Wizard erneut zeigen</value>
|
<value>Wizard erneut zeigen</value>
|
||||||
</data>
|
</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">
|
<data name="Export_Heading" xml:space="preserve">
|
||||||
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
|
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -222,6 +222,12 @@
|
|||||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||||
<value>Show wizard again</value>
|
<value>Show wizard again</value>
|
||||||
</data>
|
</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">
|
<data name="Export_Heading" xml:space="preserve">
|
||||||
<value>Export (GDPR Art. 15 — Right of access)</value>
|
<value>Export (GDPR Art. 15 — Right of access)</value>
|
||||||
</data>
|
</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)
|
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)
|
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ HellionArctic.Slug, HellionArctic.Build() },
|
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||||
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
||||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
|
||||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
|
||||||
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
|
||||||
{ NightBlue.Slug, NightBlue.Build() },
|
{ NightBlue.Slug, NightBlue.Build() },
|
||||||
|
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||||
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
||||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
{ CrystalNocturne.Slug, CrystalNocturne.Build() },
|
||||||
{ MintGrove.Slug, MintGrove.Build() },
|
{ MintGrove.Slug, MintGrove.Build() },
|
||||||
|
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||||
|
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||||
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ public sealed class ChatLogWindow : Window
|
|||||||
private bool PlayedClosingSound = true;
|
private bool PlayedClosingSound = true;
|
||||||
private bool DrewThisFrame;
|
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
|
private long FrameTime; // set every frame
|
||||||
internal long LastActivityTime = Environment.TickCount64;
|
internal long LastActivityTime = Environment.TickCount64;
|
||||||
|
|
||||||
@@ -268,7 +272,10 @@ 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.Log.Warning(
|
||||||
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
||||||
@@ -627,6 +634,19 @@ public sealed class ChatLogWindow : Window
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error drawing Chat Log window");
|
Plugin.Log.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
|
// Prevent recurring draw failures from constantly trying to grab
|
||||||
// input focus, which breaks every other ImGui window.
|
// input focus, which breaks every other ImGui window.
|
||||||
Activate = false;
|
Activate = false;
|
||||||
@@ -1620,17 +1640,21 @@ public sealed class ChatLogWindow : Window
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Active-tab underline pill (2px accent). No native ImGui underline API,
|
// 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 theme = Plugin.ThemeRegistry.Active;
|
||||||
var min = ImGui.GetItemRectMin();
|
var min = ImGui.GetItemRectMin();
|
||||||
var max = ImGui.GetItemRectMax();
|
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
|
ImGui
|
||||||
.GetWindowDrawList()
|
.GetWindowDrawList()
|
||||||
.AddRectFilled(
|
.AddRectFilled(
|
||||||
new Vector2(min.X, max.Y - pillHeight),
|
new Vector2(MathF.Round(min.X), yTop),
|
||||||
new Vector2(max.X, max.Y),
|
new Vector2(MathF.Round(max.X), yBottom),
|
||||||
ColourUtil.RgbaToAbgr(theme.Colors.Accent)
|
ColourUtil.RgbaToAbgr(theme.Colors.Accent)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,10 @@ public sealed class FirstRunWizard : Window
|
|||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
// Closing the wizard without picking anything = the user accepts
|
// OnClose fires on explicit X-click and on plugin dispose. We never
|
||||||
// whatever defaults are already in place. Mark as complete so we
|
// implicitly accept the defaults here — the explicit "Later" button
|
||||||
// don't pester them again on the next launch.
|
// does that. If the user hasn't picked a profile yet, the wizard
|
||||||
if (!Plugin.Config.FirstRunCompleted)
|
// reopens on the next plugin load.
|
||||||
{
|
|
||||||
Plugin.Config.FirstRunCompleted = true;
|
|
||||||
Plugin.SaveConfig();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
@@ -49,7 +45,12 @@ public sealed class FirstRunWizard : Window
|
|||||||
|
|
||||||
var avail = ImGui.GetContentRegionAvail();
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f;
|
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(
|
DrawCard(
|
||||||
"privacy-first",
|
"privacy-first",
|
||||||
@@ -87,6 +88,20 @@ public sealed class FirstRunWizard : Window
|
|||||||
HellionStrings.Wizard_Profile_FullHistory_Apply,
|
HellionStrings.Wizard_Profile_FullHistory_Apply,
|
||||||
ApplyFullHistory
|
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(
|
private void DrawCard(
|
||||||
|
|||||||
@@ -43,13 +43,10 @@ internal static class HellionStyle
|
|||||||
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
||||||
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
||||||
|
|
||||||
// ChildBg alpha: child areas rendered inside ChatLogWindow would
|
// ChildBg alpha resolution lives in HellionStyleHelpers so the
|
||||||
// multiply their alpha with WindowBg, making 50% opacity appear
|
// threshold logic can be covered by a pure-helper test in the
|
||||||
// ~75% solid. At full opacity the theme's alpha is preserved; below
|
// build suite.
|
||||||
// it ChildBg goes fully transparent so only WindowBg sets the final
|
var childBgWithAlpha = HellionStyleHelpers.ResolveChildBgAlpha(c.ChildBg, windowOpacity);
|
||||||
// coverage.
|
|
||||||
var childBgAlpha = windowOpacity >= 0.999f ? (c.ChildBg & 0xFFu) : 0u;
|
|
||||||
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | childBgAlpha;
|
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -199,12 +199,12 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (ImGui.Button(buttonLabel2))
|
if (ImGui.Button(buttonLabel2))
|
||||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
|
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/infiii");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
if (ImGui.Button(buttonLabel))
|
if (ImGui.Button(buttonLabel))
|
||||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/lojewalo");
|
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/lojewalo");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!save)
|
if (!save)
|
||||||
|
|||||||
@@ -79,11 +79,13 @@ internal sealed class SettingsOverview
|
|||||||
// 110f accommodates two-line subtexts; wrap width is matched in DrawCard.
|
// 110f accommodates two-line subtexts; wrap width is matched in DrawCard.
|
||||||
var cardHeight = 110f;
|
var cardHeight = 110f;
|
||||||
|
|
||||||
|
// One draw-list lookup per frame instead of one per card.
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
var cardDefs = BuildCardDefs();
|
var cardDefs = BuildCardDefs();
|
||||||
for (var i = 0; i < cardDefs.Length; i++)
|
for (var i = 0; i < cardDefs.Length; i++)
|
||||||
{
|
{
|
||||||
var (icon, title, subtext) = cardDefs[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)
|
if ((i + 1) % columns != 0 && i != cardDefs.Length - 1)
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -96,7 +98,8 @@ internal sealed class SettingsOverview
|
|||||||
string title,
|
string title,
|
||||||
string subtext,
|
string subtext,
|
||||||
float w,
|
float w,
|
||||||
float h
|
float h,
|
||||||
|
ImDrawListPtr drawList
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// BeginGroup makes the card a single layout item so SameLine works
|
// BeginGroup makes the card a single layout item so SameLine works
|
||||||
@@ -108,8 +111,7 @@ internal sealed class SettingsOverview
|
|||||||
var hovered = ImGui.IsItemHovered();
|
var hovered = ImGui.IsItemHovered();
|
||||||
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
||||||
|
|
||||||
var draw = ImGui.GetWindowDrawList();
|
drawList.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||||
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
|
||||||
|
|
||||||
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
||||||
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
||||||
@@ -120,15 +122,15 @@ internal sealed class SettingsOverview
|
|||||||
|
|
||||||
using (_window.Plugin.FontManager.FontAwesome.Push())
|
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
|
// 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.
|
// to avoid expanding the group bounds and breaking SameLine in the card row.
|
||||||
var subtextWrapWidth = w - 32f;
|
var subtextWrapWidth = w - 32f;
|
||||||
draw.AddText(
|
drawList.AddText(
|
||||||
ImGui.GetFont(),
|
ImGui.GetFont(),
|
||||||
ImGui.GetFontSize(),
|
ImGui.GetFontSize(),
|
||||||
subtextPos,
|
subtextPos,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ internal sealed class Information : ISettingsTab
|
|||||||
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
||||||
Dalamud.Utility.Util.OpenLink(
|
Plugin.PlatformUtil.OpenLink(
|
||||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues"
|
"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.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
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);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ internal sealed class Information : ISettingsTab
|
|||||||
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
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);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
|
|||||||
@@ -79,12 +79,12 @@ internal sealed class Integrations : ISettingsTab
|
|||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo))
|
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo))
|
||||||
{
|
{
|
||||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificRepo);
|
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificRepo);
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkAuthor))
|
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkAuthor))
|
||||||
{
|
{
|
||||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificAuthor);
|
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificAuthor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ internal sealed class Integrations : ISettingsTab
|
|||||||
|
|
||||||
if (ImGui.Button(HellionStrings.Settings_Integrations_GotAnIdea_LinkLabel))
|
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");
|
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
Dalamud.Utility.Util.OpenLink(dir);
|
Plugin.PlatformUtil.OpenLink(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
@@ -144,16 +144,22 @@ internal sealed class StatusBar
|
|||||||
ImGui.TextUnformatted(_cachedTellsText);
|
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 versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
|
||||||
var versionWidth = ImGui.CalcTextSize(versionText).X;
|
var versionWidth = ImGui.CalcTextSize(versionText).X;
|
||||||
var contentRegionMax = ImGui.GetContentRegionMax().X;
|
var contentRegionMax = ImGui.GetContentRegionMax().X;
|
||||||
|
const float MinOtherSlotsWidth = 200f;
|
||||||
|
if (contentRegionMax - versionWidth > MinOtherSlotsWidth)
|
||||||
|
{
|
||||||
ImGui.SameLine(contentRegionMax - versionWidth);
|
ImGui.SameLine(contentRegionMax - versionWidth);
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(versionText);
|
ImGui.TextUnformatted(versionText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void DrawDot(uint rgba)
|
private static void DrawDot(uint rgba)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
}
|
||||||
@@ -254,6 +254,17 @@ internal static class ImGuiUtil
|
|||||||
return end;
|
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(
|
internal static bool IconButton(
|
||||||
FontAwesomeIcon icon,
|
FontAwesomeIcon icon,
|
||||||
string? id = null,
|
string? id = null,
|
||||||
@@ -268,10 +279,7 @@ internal static class ImGuiUtil
|
|||||||
bool ret;
|
bool ret;
|
||||||
using (Plugin.FontManager.FontAwesome.Push())
|
using (Plugin.FontManager.FontAwesome.Push())
|
||||||
{
|
{
|
||||||
var size = Vector2.Zero;
|
var size = width > 0 ? new Vector2(width, 0f) : Vector2.Zero;
|
||||||
if (width > 0)
|
|
||||||
size.X = width - 2 * ImGui.GetStyle().CellPadding.X;
|
|
||||||
|
|
||||||
ret = ImGui.Button(label, size);
|
ret = ImGui.Button(label, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public static class WrapperUtil
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Plugin.Log.Debug($"Opening URI {uri} in default browser");
|
Plugin.Log.Debug($"Opening URI {uri} in default browser");
|
||||||
Dalamud.Utility.Util.OpenLink(uri.ToString());
|
Plugin.PlatformUtil.OpenLink(uri.ToString());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||||
[](https://github.com/goatcorp/Dalamud)
|
[](https://github.com/goatcorp/Dalamud)
|
||||||
[](https://dotnet.microsoft.com/)
|
[](https://dotnet.microsoft.com/)
|
||||||
[](https://www.finalfantasyxiv.com/)
|
[](https://www.finalfantasyxiv.com/)
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Version 1.4.4** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
**Version 1.4.6** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||||
|
|
||||||
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
|
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
|
||||||
@@ -102,7 +102,7 @@ Hellion Chat is developed under **Hellion Forge**, the specialized modding and p
|
|||||||
#### Custom Themes (v1.1.0)
|
#### Custom Themes (v1.1.0)
|
||||||
|
|
||||||
HellionChat ships a theme engine with ten built-in themes (Hellion Arctic, Hellion Spectrum, Chat 2 Classic, Event
|
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
|
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
|
[`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.
|
blindness) based on the Wong/Okabe-Ito palette.
|
||||||
@@ -286,16 +286,20 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
|
|||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
**Version 1.4.4** — Threading and IPC safety polish on top of the v1.4.3 async-load foundation. The
|
**Version 1.4.6** — Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop and
|
||||||
`AutoTellTabsService` hot-path getter now reads from an `Interlocked` counter instead of taking a lock on every
|
pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier reflow check and a markdownlint pass at
|
||||||
render frame, with a resync hook for the snapshot-restore path in `SaveConfig` and a pure-helper test mirror in the
|
the pre-push gate. `FontManager`'s font-fallback catch-filter now covers `InvalidOperationException` and
|
||||||
Build-Suite repo. The Honorific integration carries per-method threading banners so the framework-thread invariant is
|
`ArgumentException` on top of the IO triad, so a corrupted font config no longer takes down the atlas build.
|
||||||
visible at the call site, and an unsubscribe failure now logs at Warning instead of Debug — a leaked subscription
|
`BrandingLinks` and `IntegrationLinks` URLs validate themselves on plugin load — a typo in a future URL rotation throws
|
||||||
across plugin reloads is exactly the kind of thing that should not be silent. The AutoTranslate warmup thread is
|
at startup instead of failing silently when a user clicks the broken button. Cherry-picked from ChatTwo upstream
|
||||||
finally marked `IsBackground = true`, matching the pattern used in `MessageManager` and `Plugin.RetentionSweep` since
|
`f35b7d3`: `Chat.SetChannel` no longer leaks the native `Utf8String` when the linkshell check rejects the channel, and
|
||||||
v1.4.0. The privacy filter logs once per unknown ChatType so a future patch's added channel does not drop off the
|
`Tab.Clone` now deep-clones `UsedChannel` and `TellTarget` (the previous reference copy let PopOut and Temp tabs mutate
|
||||||
radar, and new installs default `PrivacyPersistUnknownChannels` to `true` as a failsafe; existing configs keep their
|
each other's channel state). The active-tab underline pill scales with DPI and rounds to physical pixels for crisp
|
||||||
explicit choice. No schema bump, no migration. Fifth sub-patch of the v1.4.x polish sweep series (as of 2026-05-12).
|
rendering above 100 % DPI. Internal items: `HellionStyle` ChildBgAlpha extracted to a testable helper,
|
||||||
|
`Plugin.SaveConfig` clones only the temp-tab subset, `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. Seventh sub-patch of the v1.4.x polish sweep series (as of
|
||||||
|
2026-05-12).
|
||||||
|
|
||||||
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
||||||
|
|
||||||
|
|||||||
+88
-11
@@ -10,6 +10,83 @@ to the release pages for details.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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)
|
## 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
|
Fifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock
|
||||||
@@ -20,21 +97,21 @@ unknown ChatType shows up.
|
|||||||
in sync with `Config.Tabs` from inside the existing mutation paths. `Initialize()` seeds the counter from the
|
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
|
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
|
`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
|
- `HonorificService` per-method threading banners replace the block comment at the bottom of the file. Each IPC callback
|
||||||
callback (`TryInitialPull`, `OnTitleChanged`, `OnReady`, `OnDisposing`, `TryUnsubscribe`) and the `CurrentTitle`
|
(`TryInitialPull`, `OnTitleChanged`, `OnReady`, `OnDisposing`, `TryUnsubscribe`) and the `CurrentTitle` field carry a
|
||||||
field carry a one-line `// Thread:` annotation so the framework-thread invariant is visible at the call site
|
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
|
- `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
|
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
|
- `AutoTranslate.PreloadCache` thread now has `IsBackground = true` and a thread name. Without `IsBackground` the warmup
|
||||||
warmup blocks plugin unload (typically 100-300 ms). Pattern-match to `MessageManager` (F6.1) and `Plugin.RetentionSweep`
|
blocks plugin unload (typically 100-300 ms). Pattern-match to `MessageManager` (F6.1) and `Plugin.RetentionSweep`
|
||||||
(F9.3), both since v1.4.0
|
(F9.3), both since v1.4.0
|
||||||
- `Configuration.IsAllowedForStorage` adds a one-line `Plugin.Log.Warning` for the first occurrence of any ChatType
|
- `Configuration.IsAllowedForStorage` adds a one-line `Plugin.Log.Warning` for the first occurrence of any ChatType that
|
||||||
that isn't in `PrivacyPersistChannels`. Dedup via a `NonSerialized` `HashSet<ChatType>`, so the warning fires once
|
isn't in `PrivacyPersistChannels`. Dedup via a `NonSerialized` `HashSet<ChatType>`, so the warning fires once per
|
||||||
per runtime — not once per frame, not once per install. Failsafe routing through `PrivacyPersistUnknownChannels`
|
runtime — not once per frame, not once per install. Failsafe routing through `PrivacyPersistUnknownChannels` is
|
||||||
is unchanged
|
unchanged
|
||||||
- `PrivacyPersistUnknownChannels` field default flipped from `false` to `true` for new installs via a constant in
|
- `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
|
`PrivacyDefaults`. Existing configs keep their explicit choice — the deserializer overrides the initializer. No schema
|
||||||
schema bump, no migration, no first-run banner
|
bump, no migration, no first-run banner
|
||||||
|
|
||||||
Modding & support: join Hellion Forge — <https://discord.gg/X9V7Kcv5gR>
|
Modding & support: join Hellion Forge — <https://discord.gg/X9V7Kcv5gR>
|
||||||
|
|
||||||
|
|||||||
+46
-4
@@ -10,14 +10,56 @@ the plugin's privacy-first scope during brainstorming.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Next Cycle (v1.4.4)
|
## Next Cycle (v1.4.7)
|
||||||
|
|
||||||
**Window-Lazy-Open + Render-Init-Cost Optimisation** — take the `IAsyncDalamudPlugin` foundation laid in v1.4.3 and turn
|
**Backlog Cleanup.** Roll up the remaining audit items deferred from v1.4.0–v1.4.6 and the new entries surfaced during
|
||||||
it into wins users can actually feel. Window construction deferred until first open, render-path init cost reduced in
|
v1.4.6 (notably the `Plugin.Log` indirection that would unlock fully isolated `MessageStore` construction tests, plus
|
||||||
the first frames. Concrete candidates and size estimates will be consolidated in the v1.4.4 brainstorm.
|
follow-up scope hinted at in the ChatTwo upstream f35b7d3 cherry-picks). Scope is consolidated during brainstorm.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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)
|
## 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:
|
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
|
**don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual
|
||||||
hierarchy.
|
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
|
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.
|
intentionally ships without `chatChannels` so the user keeps their existing picks.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"Author": "Jon Kazama (Hellion Forge)",
|
"Author": "Jon Kazama (Hellion Forge)",
|
||||||
"Name": "Hellion Chat",
|
"Name": "Hellion Chat",
|
||||||
"InternalName": "HellionChat",
|
"InternalName": "HellionChat",
|
||||||
"AssemblyVersion": "1.4.4.0",
|
"AssemblyVersion": "1.4.6.0",
|
||||||
"Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
|
"Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
"CanUnloadAsync": false,
|
"CanUnloadAsync": false,
|
||||||
"LoadPriority": 0,
|
"LoadPriority": 0,
|
||||||
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
|
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
|
||||||
"Changelog": "**v1.4.4 — Threading and IPC safety polish (2026-05-12)**\n\nFifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock falls away, and the privacy filter speaks up when an unknown ChatType shows up.\n\n- AutoTellTabs hot-path getter uses an Interlocked counter instead of taking the lock on every read\n- Honorific integration: per-method threading banners, plus Warning-level log on unsubscribe failure\n- AutoTranslate warmup thread marked IsBackground so plugin unload doesn't wait for it\n- PrivacyFilter logs once per unknown ChatType so a future patch's added channel doesn't drop off the radar\n- New installs persist unknown channels by default; existing configs keep their explicit choice\n\n---\n\n**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy 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.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\n**v1.4.2 — Smoother frames in the chat log**\n\nPer-frame allocations in the chat-log render path eliminated. 2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.\n\n- Card-mode: theme/border invariants hoisted out of the per-message loop\n- Auto-tell tab tint and icon cached per tab\n- Status bar aggregation runs on ~1% of frames instead of every frame\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
|
"Changelog": "**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance 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.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- 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\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- 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\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- 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)\n- 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\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth 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.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- 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\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system font if the embedded Hellion font resource is missing — logs a Warning\n\n---\n\n**v1.4.4 — Threading and IPC safety polish (2026-05-12)**\n\nFifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock falls away, and the privacy filter speaks up when an unknown ChatType shows up.\n\n- AutoTellTabs hot-path getter uses an Interlocked counter instead of taking the lock on every read\n- Honorific integration: per-method threading banners, plus Warning-level log on unsubscribe failure\n- AutoTranslate warmup thread marked IsBackground so plugin unload doesn't wait for it\n- PrivacyFilter logs once per unknown ChatType so a future patch's added channel doesn't drop off the radar\n- New installs persist unknown channels by default; existing configs keep their explicit choice\n\n---\n\n**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy 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.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
|
||||||
"AcceptsFeedback": true,
|
"AcceptsFeedback": true,
|
||||||
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.4/latest.zip",
|
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip",
|
||||||
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.4/latest.zip",
|
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip",
|
||||||
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.4/latest.zip",
|
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.6/latest.zip",
|
||||||
"TestingAssemblyVersion": "1.4.4.0",
|
"TestingAssemblyVersion": "1.4.6.0",
|
||||||
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
||||||
"ImageUrls": [
|
"ImageUrls": [
|
||||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
||||||
|
|||||||
+12
-2
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# preflight.sh — pre-push gate. Blocks A/B/C verify config drift; Block D is a
|
# 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
|
# headless `dotnet build` to catch compile-time API drift; Block E runs
|
||||||
# in the local Build-Suite repo and is NOT part of this preflight.
|
# `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
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
@@ -19,4 +21,12 @@ echo "==> preflight: Block C — changelog sync"
|
|||||||
echo "==> preflight: Block D — plugin compile health"
|
echo "==> preflight: Block D — plugin compile health"
|
||||||
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
|
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"
|
echo "==> preflight: ALL GREEN"
|
||||||
|
|||||||
Reference in New Issue
Block a user