diff --git a/.github/forge-posts/v1.5.5.md b/.github/forge-posts/v1.5.5.md
new file mode 100644
index 0000000..ff72974
--- /dev/null
+++ b/.github/forge-posts/v1.5.5.md
@@ -0,0 +1,11 @@
+---
+subtitle: "Backlog-Sync Tab-Features"
+versionsnatur: "Bundle-Patch (Hälfte 1 von 2)"
+---
+- **Fehlgeschlagener Tell.** Geht ein gesendeter Tell nicht durch (Empfänger offline, in einer Instanz oder blockiert), erscheint jetzt ein Warn-Toast statt dass die Systemmeldung durchrauscht. Abschaltbar in den Einstellungen unter Chat.
+- **Ton pro Tab.** Jeder Chat-Tab kann einen Benachrichtigungston spielen, wenn eine Nachricht eintrifft, während ein anderer Tab aktiv ist. Zur Wahl stehen die 16 Spiel-Chat-Sounds oder drei mitgelieferte Hellion-Sounds, mit einem Vorhör-Knopf. Standardmäßig aus, hört auf den globalen Sound-Schalter.
+- **Tab umbenennen.** Das Umbenennen-Feld im Rechtsklick-Menü fokussiert sich beim Öffnen von selbst und nimmt jetzt bis zu 512 Zeichen.
+- **Sprung ans Ende.** In der Chat-Kopfleiste erscheint ein Knopf, sobald man vom aktuellen Ende weggescrollt ist. Ein Klick springt zurück zur jüngsten Nachricht.
+- **Karten- und Item-Links.** Kartenmarkierung und verlinktes Item lassen sich aus dem Rechtsklick-Menü der Chat-Eingabe einfügen.
+- **Fuchs-Banner.** Das Hellion-Forge-Fuchs-Motiv im Einrichtungs-Assistenten und im Informations-Tab ist jetzt ein echtes Bild statt ASCII-Kunst.
+- Schema-Bump auf v18, rein additiv.
diff --git a/HellionChat/Branding/FoxBannerTexture.cs b/HellionChat/Branding/FoxBannerTexture.cs
new file mode 100644
index 0000000..353dc31
--- /dev/null
+++ b/HellionChat/Branding/FoxBannerTexture.cs
@@ -0,0 +1,22 @@
+using Dalamud.Interface.Textures;
+
+namespace HellionChat.Branding;
+
+// UI sibling of HellionForgeAscii.FoxMini: the embedded Hellion Forge fox
+// banner PNG. Uses ITextureProvider.GetFromManifestResource, a "Get" shared
+// texture, so Dalamud owns the cache and lifetime. No manual dispose, no async
+// handling in the plugin. Static to mirror HellionForgeAscii (zero injectable
+// deps; Plugin.TextureProvider is a static [PluginService]).
+internal static class FoxBannerTexture
+{
+ private const string ResourceName = "HellionChat.Branding.fox-banner.png";
+
+ // Resolved fresh on every access. Dalamud keeps the shared texture cached
+ // internally and decodes it asynchronously, so GetWrapOrDefault() returns
+ // null for the first few frames until the decode finishes.
+ public static ISharedImmediateTexture Shared =>
+ Plugin.TextureProvider.GetFromManifestResource(
+ typeof(FoxBannerTexture).Assembly,
+ ResourceName
+ );
+}
diff --git a/HellionChat/Branding/HellionForgeAscii.cs b/HellionChat/Branding/HellionForgeAscii.cs
index da98572..7bbf04d 100644
--- a/HellionChat/Branding/HellionForgeAscii.cs
+++ b/HellionChat/Branding/HellionForgeAscii.cs
@@ -1,25 +1,18 @@
namespace HellionChat.Branding;
-// Lazy-loaded provenance art that ships embedded with the DLL. Two
-// variants:
+// Lazy-loaded ASCII art that ships embedded with the DLL.
//
-// - FoxBanner: the full-size silhouette with "Hellion Forge" inside
-// the body — rendered in the first-run wizard and the Information
-// tab as a small "about the makers" anchor.
// - FoxMini: the four-line fox-head + curly-tail that gets stitched
// into the DI-logger bootstrap line so an xllog reader sees the
// same signature on every plugin load.
//
-// Both files live as embedded resources under HellionChat.Branding.* so
-// the plugin DLL is self-contained — no on-disk asset lookup that could
+// The file lives as an embedded resource under HellionChat.Branding.* so
+// the plugin DLL is self-contained; no on-disk asset lookup that could
// silently miss after a partial deploy.
internal static class HellionForgeAscii
{
- private static string? _foxBanner;
private static string? _foxMini;
- public static string FoxBanner => _foxBanner ??= Load("HellionChat.Branding.fox-banner.txt");
-
public static string FoxMini => _foxMini ??= Load("HellionChat.Branding.fox-mini.txt");
private static string Load(string resourceName)
diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs
index e93adaa..af49a91 100755
--- a/HellionChat/Configuration.cs
+++ b/HellionChat/Configuration.cs
@@ -34,7 +34,7 @@ public class ConfigKeyBind
[Serializable]
public class Configuration : IPluginConfiguration
{
- private const int LatestVersion = 17;
+ private const int LatestVersion = 18;
public int Version { get; set; } = LatestVersion;
@@ -187,6 +187,9 @@ public class Configuration : IPluginConfiguration
public bool CollapseKeepUniqueLinks;
public bool SymbolPickerEnabled = true;
public bool PlaySounds = true;
+
+ // Toast when a tell the user sent could not be delivered.
+ public bool NotifyFailedTell = true;
public bool KeepInputFocus = true;
public int MaxLinesToRender = 2_500; // 1-10000
public bool Use24HourClock = true;
@@ -282,6 +285,7 @@ public class Configuration : IPluginConfiguration
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
SymbolPickerEnabled = other.SymbolPickerEnabled;
PlaySounds = other.PlaySounds;
+ NotifyFailedTell = other.NotifyFailedTell;
KeepInputFocus = other.KeepInputFocus;
MaxLinesToRender = other.MaxLinesToRender;
Use24HourClock = other.Use24HourClock;
@@ -443,6 +447,10 @@ public class Tab
public bool AllSenderMessages;
public TellTarget TellTarget = TellTarget.Empty();
+ // Per-tab notification sound for messages arriving in an inactive tab.
+ public bool EnableNotificationSound;
+ public uint NotificationSoundId = 1;
+
[NonSerialized]
public uint Unread;
@@ -561,6 +569,8 @@ public class Tab
IsPinned = IsPinned,
AllSenderMessages = AllSenderMessages,
TellTarget = TellTarget.Clone(),
+ EnableNotificationSound = EnableNotificationSound,
+ NotificationSoundId = NotificationSoundId,
IsGreeted = IsGreeted,
};
}
diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj
index a9071a9..4aede89 100644
--- a/HellionChat/HellionChat.csproj
+++ b/HellionChat/HellionChat.csproj
@@ -1,7 +1,7 @@
- 1.5.4
+ 1.5.5
enable
enable
@@ -26,6 +26,11 @@
+
+
@@ -58,8 +63,18 @@
Inter-OFL.txt
-
- HellionChat.Branding.fox-banner.txt
+
+ HellionChat.Branding.fox-banner.png
+
+
+
+ HellionChat.Sounds.notification-1.wav
+
+
+ HellionChat.Sounds.notification-2.wav
+
+
+ HellionChat.Sounds.notification-3.wav
HellionChat.Branding.fox-mini.txt
diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml
index cf781c5..d2d8834 100755
--- a/HellionChat/HellionChat.yaml
+++ b/HellionChat/HellionChat.yaml
@@ -35,6 +35,37 @@ tags:
- Replacement
- Privacy
changelog: |-
+ **v1.5.5 — Upstream-Sync Tab-Features (2026-05-21)**
+
+ A backlog-sync cycle: inherited tab-feature items plus a new fox
+ banner image and custom notification sounds.
+
+ User-visible:
+
+ - Failed tells now raise a warning toast when a message you sent
+ could not be delivered (recipient offline, in an instance, or
+ blocking you). Toggle in Settings, Chat tab.
+ - Per-tab notification sound: each tab can play a sound when a
+ message arrives while you are looking at a different tab. Pick
+ one of the 16 game chat sounds or one of three bundled Hellion
+ sounds, with a preview button to hear it. Off by default,
+ respects the global sound toggle.
+ - The tab rename field in the right-click menu now focuses
+ itself when the menu opens and accepts up to 512 characters,
+ matching the settings-tab rename.
+ - A jump-to-latest button appears in the chat log header while
+ you are scrolled up from the live end.
+ - Map flags and item links can be inserted into the chat input
+ from its right-click menu.
+ - The Hellion Forge fox banner in the first-run wizard and the
+ Information tab is now a real image instead of ASCII art.
+
+ Schema bumped to v18 (additive fields only, no data migration).
+
+ Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
+
+ ---
+
**v1.5.4 — Polish and Motion (2026-05-20)**
A polish cycle: smoother theme switching, faster theme and tab
@@ -182,54 +213,4 @@ changelog: |-
---
- **v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**
-
- Hybrid FontManager refactor plus an embedded provenance mark.
-
- What changes under the hood:
-
- - FontManager handle creation moves into the ctor inside a single
- atlas.SuppressAutoRebuild() block. The font atlas now builds once
- per plugin load instead of four to five times — less CPU and GPU
- pressure in the first seconds after a reload, less atlas texture
- memory churn.
- - Hybrid property model: Axis, AxisItalic and FontAwesome become
- init-only handles. RegularFont and ItalicFont stay mutable because
- the eight font settings still need to replace them at runtime —
- that path is funnelled through RebuildDelegateFonts() now and
- runs without a plugin reload.
- - FontAwesome reuses Dalamud's UiBuilder.IconFontFixedWidthHandle
- instead of building its own atlas slot. One delegate-build step
- less in the ctor.
- - BuildFontsAsync and BuildFonts are removed; the live mutation
- path is RebuildDelegateFonts() now.
- - Two FontManager self-test steps registered with /xlperf: ctor
- smoke (every handle non-null after Phase-1 resolve, no atlas
- load-exception) and push smoke (Push() returns without throwing).
-
- Honorific full-gradient port (originally the v1.5.1 main item) was
- dropped: Honorific 3.2 exposes no IPC for the rendered gradient
- frame, and an in-plugin port of the colour palette was declined.
- The integration stays at the v1.4.7 glow-only shape.
-
- User-visible:
-
- - Hellion Forge signature: a small fox-head ASCII silhouette is
- emitted to /xllog on every plugin load, and a full fox banner
- with "Hellion Forge" set inside the body is available as a
- folded TreeNode in the First-Run Wizard and Settings ->
- Information tab. Drawn by Julia Moon, embedded in the plugin DLL.
- - No settings changes, no migration. v17 stays.
-
- Note on performance: the cross-plugin baseline target from v1.5.0
- (matching Lightless and XIVInstantMessenger at ~7 ms HITCH) did
- not land this cycle. HITCH stays around 80 ms because the cost is
- in the UiBuilder first-frame render path, not in the atlas build
- (which this cycle did reduce from 4-5 builds per load to 1). A
- first-frame render investigation is reserved for a later cycle.
-
- Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
-
- ---
-
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
diff --git a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs
index b7729f6..9ffb54f 100644
--- a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs
+++ b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs
@@ -1,4 +1,5 @@
using Dalamud.Plugin;
+using HellionChat.Integrations;
using HellionChat.Ipc;
using HellionChat.Themes;
using Microsoft.Extensions.Hosting;
@@ -85,3 +86,18 @@ internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService s
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
+
+// Eager-resolve trigger: resolving FailedTellNotifier in this adapter's ctor
+// enables its game hook during host startup. StartAsync itself is a no-op.
+internal sealed class FailedTellNotifierInitHostedService(FailedTellNotifier notifier)
+ : IHostedService
+{
+ // No-op adapter: the ctor dependency above is the actual eager-resolve
+ // trigger. Field kept to match the IpcManager/TypingIpc/ExtraChat no-op
+ // adapters and to avoid the CS9113 unread-parameter warning.
+ private readonly FailedTellNotifier _notifier = notifier;
+
+ public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
diff --git a/HellionChat/Integrations/CustomAudioPlayer.cs b/HellionChat/Integrations/CustomAudioPlayer.cs
new file mode 100644
index 0000000..320b5fd
--- /dev/null
+++ b/HellionChat/Integrations/CustomAudioPlayer.cs
@@ -0,0 +1,146 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.Logging;
+using NAudio.Wave;
+
+namespace HellionChat.Integrations;
+
+// Plays the three bundled WAV notification sounds via NAudio WaveOutEvent.
+// WaveOutEvent/WinMM is the correct backend for FFXIV on Wine: it works
+// without Media Foundation (which Wine does not support for MP3/AAC).
+//
+// Volume is fixed at 0.8. No per-user slider in this iteration so we can
+// ship quickly and gather feedback before adding UX complexity.
+internal sealed class CustomAudioPlayer : IDisposable
+{
+ // Sound bytes are read once at construction so each Play() wraps a fresh
+ // MemoryStream rather than re-reading the manifest stream (which becomes
+ // unreadable after the first read and would require Seek support).
+ private readonly byte[][] _soundData;
+ private readonly ILogger _logger;
+
+ private WaveOutEvent? _outputDevice;
+ private WaveFileReader? _reader;
+ private readonly object _lock = new();
+
+ public CustomAudioPlayer(ILogger logger)
+ {
+ _logger = logger;
+ _soundData = new byte[3][];
+
+ for (var i = 0; i < 3; i++)
+ {
+ var resourceName = $"HellionChat.Sounds.notification-{i + 1}.wav";
+ using var stream = typeof(CustomAudioPlayer).Assembly.GetManifestResourceStream(
+ resourceName
+ );
+ if (stream is null)
+ {
+ _logger.LogWarning(
+ "Embedded sound resource not found: {Resource}. "
+ + "Custom sound {Index} will be silent.",
+ resourceName,
+ i + 1
+ );
+ _soundData[i] = Array.Empty();
+ continue;
+ }
+
+ using var ms = new MemoryStream();
+ stream.CopyTo(ms);
+ _soundData[i] = ms.ToArray();
+ }
+ }
+
+ // customIndex is 1, 2, or 3, matching the sound file suffix.
+ // Stops any currently playing sound before starting the new one.
+ // NAudio playback runs on its own thread; this method returns immediately.
+ public void Play(int customIndex)
+ {
+ if (customIndex < 1 || customIndex > 3)
+ {
+ _logger.LogWarning(
+ "CustomAudioPlayer.Play called with out-of-range index {Index}",
+ customIndex
+ );
+ return;
+ }
+
+ var data = _soundData[customIndex - 1];
+ if (data.Length == 0)
+ {
+ _logger.LogWarning(
+ "Sound data for index {Index} is empty; skipping playback",
+ customIndex
+ );
+ return;
+ }
+
+ lock (_lock)
+ {
+ try
+ {
+ StopCurrent();
+
+ var ms = new MemoryStream(data, writable: false);
+ _reader = new WaveFileReader(ms);
+
+ _outputDevice = new WaveOutEvent();
+ // Init opens the device and creates the WinMM handle. Volume
+ // must be set after Init, otherwise waveOutSetVolume fails with
+ // InvalidHandle.
+ _outputDevice.Init(_reader);
+ _outputDevice.Volume = 0.8f;
+ _outputDevice.Play();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(
+ ex,
+ "Failed to play custom notification sound {Index}",
+ customIndex
+ );
+ StopCurrent();
+ }
+ }
+ }
+
+ // Stops and tears down the active WaveOutEvent + WaveFileReader without
+ // throwing. Called on Play (to interrupt previous sound) and from Dispose.
+ // Guards Stop() with a PlaybackState check because waveOutReset blocks even
+ // when playback already finished; under Wine this can stall the WinMM
+ // callback thread if many sounds arrive in quick succession.
+ private void StopCurrent()
+ {
+ try
+ {
+ if (_outputDevice?.PlaybackState == PlaybackState.Playing)
+ _outputDevice.Stop();
+ _outputDevice?.Dispose();
+ _outputDevice = null;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Exception while stopping current WaveOutEvent");
+ }
+
+ try
+ {
+ _reader?.Dispose();
+ _reader = null;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Exception while disposing WaveFileReader");
+ }
+ }
+
+ // At plugin unload the PendingMessageThread is already cancelled and the
+ // draw loop is gone, so _lock is uncontended here. Calling StopCurrent
+ // outside the lock avoids holding it across the blocking waveOutReset /
+ // WaveOutEvent.Dispose, which can freeze on Wine during unload.
+ public void Dispose()
+ {
+ StopCurrent();
+ }
+}
diff --git a/HellionChat/Integrations/FailedTellNotifier.cs b/HellionChat/Integrations/FailedTellNotifier.cs
new file mode 100644
index 0000000..35ad369
--- /dev/null
+++ b/HellionChat/Integrations/FailedTellNotifier.cs
@@ -0,0 +1,74 @@
+using System;
+using Dalamud.Hooking;
+using Dalamud.Interface.ImGuiNotification;
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using HellionChat._Helpers;
+using HellionChat.Resources;
+using HellionChat.Util;
+using Microsoft.Extensions.Logging;
+
+namespace HellionChat.Integrations;
+
+// A minimal, failed-tell-specific game hook. A locale-robust "tell failed"
+// signal is not reachable over the processed message stream (Message carries
+// no LogMessage row id, ChatCode 60 is too broad). This hooks the one
+// ShowLogMessageString overload and toasts on a pinned id set. It is NOT the
+// broad ad-block hook layer.
+internal sealed class FailedTellNotifier : IDisposable
+{
+ private readonly ILogger _logger;
+ private readonly Hook? _hook;
+
+ public unsafe FailedTellNotifier(ILogger logger)
+ {
+ _logger = logger;
+
+ // Creating/enabling a hook is safe off the framework thread (the
+ // ctor runs during host startup on the framework thread,
+ // eager-resolved via FailedTellNotifierInitHostedService).
+ _hook =
+ Plugin.GameInteropProvider.HookFromAddress(
+ RaptureLogModule.MemberFunctionPointers.ShowLogMessageString,
+ ShowLogMessageStringDetour
+ );
+ _hook.Enable();
+ }
+
+ private unsafe void ShowLogMessageStringDetour(
+ RaptureLogModule* module,
+ uint logMessageId,
+ Utf8String* value
+ )
+ {
+ try
+ {
+ if (
+ FailedTellMatcher.ShouldNotify(
+ logMessageId,
+ Plugin.Config.NotifyFailedTell,
+ FailedTellMatcher.FailedTellLogMessageIds
+ )
+ )
+ {
+ var recipient = value is null ? string.Empty : value->ToString();
+ var content = string.IsNullOrEmpty(recipient)
+ ? HellionStrings.FailedTell_Notification_Generic
+ : string.Format(HellionStrings.FailedTell_Notification_Named, recipient);
+ WrapperUtil.AddNotification(content, NotificationType.Warning);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "FailedTellNotifier detour threw");
+ }
+
+ _hook!.Original(module, logMessageId, value);
+ }
+
+ public void Dispose()
+ {
+ _hook?.Disable();
+ _hook?.Dispose();
+ }
+}
diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs
index fc11a2f..df91241 100644
--- a/HellionChat/MessageManager.cs
+++ b/HellionChat/MessageManager.cs
@@ -7,7 +7,9 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using HellionChat._Helpers;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
@@ -330,6 +332,7 @@ internal class MessageManager : IAsyncDisposable
Store.UpsertMessage(message);
var currentMatches = Plugin.CurrentTab.Matches(message);
+ uint? notificationSound = null;
foreach (var tab in Plugin.Config.Tabs)
{
var unread = !(
@@ -337,7 +340,49 @@ internal class MessageManager : IAsyncDisposable
);
if (tab.Matches(message))
+ {
tab.AddMessage(message, unread);
+
+ // Per-tab notification sound. Fire once for the first inactive
+ // tab that wants it, keeping a message matching several
+ // background tabs from stacking sounds.
+ // TEST-MIRROR: ../_Helpers/TabSoundDecision.cs
+ if (
+ notificationSound is null
+ && TabSoundDecision.ShouldPlay(
+ Plugin.CurrentTab == tab,
+ tab.EnableNotificationSound,
+ Plugin.Config.PlaySounds
+ )
+ )
+ {
+ notificationSound = tab.NotificationSoundId;
+ }
+ }
+ }
+
+ if (notificationSound is { } soundId)
+ {
+ if (soundId is >= 1 and <= 16)
+ {
+ // ProcessMessage runs on the PendingMessageThread worker; the native
+ // UIGlobals.PlaySoundEffect must be marshalled onto the framework
+ // thread (reference_dalamud_framework_thread).
+ Plugin.Framework.RunOnFrameworkThread(() =>
+ {
+ unsafe
+ {
+ UIGlobals.PlaySoundEffect(soundId);
+ }
+ });
+ }
+ else if (soundId >= 17)
+ {
+ // Custom bundled sounds (ids 17-19) go through NAudio WaveOutEvent.
+ // NAudio manages its own playback thread, so no framework marshalling needed.
+ Plugin.CustomAudioPlayer.Play((int)soundId - 16);
+ }
+ // soundId == 0 (hand-edited config) falls through: plays nothing.
}
MessageProcessed?.Invoke(message);
diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs
index fa37ccf..cbb1679 100755
--- a/HellionChat/Plugin.cs
+++ b/HellionChat/Plugin.cs
@@ -115,6 +115,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
internal Ui.StatusBar StatusBar { get; private set; } = null!;
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
+ internal Integrations.CustomAudioPlayer CustomAudioPlayer { 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.
@@ -198,10 +199,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
// point (MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize)
// do not touch either static, so the brief null-window is safe.
- // Schema gate: v1.4.x requires config v16+. Users on older schemas
- // must install v1.4.2 first to run the migration chain. v17 adds
- // Tab.IsPinned (additive, no data migration needed) so v16 configs
- // load cleanly and get their Version stamp bumped after the gate.
+ // Schema gate: v1.4.x+ requires config v16+. Users on older schemas
+ // must install v1.4.2 first to run the migration chain. v18 adds the
+ // per-tab EnableNotificationSound + NotificationSoundId fields and the
+ // top-level NotifyFailedTell flag, all additive with defaults, so
+ // v16/v17 configs load cleanly and get their Version stamp bumped
+ // after the gate.
if (Config.Version < 16)
{
throw new InvalidOperationException(
@@ -209,7 +212,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10."
);
}
- Config.Version = 17;
+ Config.Version = 18;
// Unpinned TempTabs are session-only and dropped on every load. Pinned
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
@@ -284,6 +287,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
TypingIpc = _host.Services.GetRequiredService();
ExtraChat = _host.Services.GetRequiredService();
HonorificService = _host.Services.GetRequiredService();
+ CustomAudioPlayer = _host.Services.GetRequiredService();
StatusBar = _host.Services.GetRequiredService();
MessageManager = _host.Services.GetRequiredService();
AutoTellTabsService = _host.Services.GetRequiredService();
@@ -335,6 +339,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
new SelfTests.FontPushSmokeStep(this),
new SelfTests.WizardStateSmokeStep(this),
new SelfTests.QuickPickerSelfTestStep(this),
+ new SelfTests.FoxBannerTextureSmokeStep(this),
]);
// Re-surface the wizard for existing users when a major UX
diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs
index f0c3375..62ed716 100644
--- a/HellionChat/PluginHostFactory.cs
+++ b/HellionChat/PluginHostFactory.cs
@@ -107,6 +107,12 @@ internal static class PluginHostFactory
sp.GetRequiredService>(),
sp.GetRequiredService()
));
+ services.AddSingleton(sp => new Integrations.FailedTellNotifier(
+ sp.GetRequiredService>()
+ ));
+ services.AddSingleton(sp => new Integrations.CustomAudioPlayer(
+ sp.GetRequiredService>()
+ ));
services.AddSingleton(sp => new MessageManager(
sp.GetRequiredService(),
@@ -172,6 +178,11 @@ internal static class PluginHostFactory
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
sp.GetRequiredService()
));
+ services.AddHostedService(
+ sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
+ sp.GetRequiredService()
+ )
+ );
}
}
diff --git a/HellionChat/Resources/Branding/fox-banner.png b/HellionChat/Resources/Branding/fox-banner.png
new file mode 100644
index 0000000..135acf6
Binary files /dev/null and b/HellionChat/Resources/Branding/fox-banner.png differ
diff --git a/HellionChat/Resources/Branding/fox-banner.txt b/HellionChat/Resources/Branding/fox-banner.txt
deleted file mode 100644
index 8351a91..0000000
--- a/HellionChat/Resources/Branding/fox-banner.txt
+++ /dev/null
@@ -1,68 +0,0 @@
- .:;+xXXX$$$$$$$$XXx+;:
-.X$+ .;+X$$$$$$$$$$$$$$$$$$$$$$$$$$$x:
-;$xx$$X+:... .....::+X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$;.
-X$; .:+xXXX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X:
-$$; :++xX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X;
-$$x. .+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X.
-x$$; ;$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+;::::::;x$$$$$:
-:$$$; .+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+:. .+$$$$$$$$$X+;;:
- ;$$$+. :X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X;: :$$$$$$$$$$$$$$$$X;.
- .+$$$X: ..;X$$$$$$$$$$$$$$$$$$$$$$$$$$X;.. :$$$$$$$$$$$$$$$$$$$$X:
- ;$$$$$X+::::+X$$$$$$$$$$$$$$$$$$$$$X;. .$$$$$$$$$$$$$$$$$$$$$$$X;
- +$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+: Hellion Forge x$$$$$$$$$$$$$$$$$$$$$$$$$X:
- .;x$$$$$$$$$$$$$$$$$$$$$x;: .X$$$$$$$$$$$$$$$$$$$$$$$$$$$+
- .;+$$$$$$$$$$X+;:.. .X$$$$$$$$$$$$$$$$$$$$$$$$$$$$+
- .X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$;
- .X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X
- x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X
- ;$$$$$$xx$$$$$$$$$$$$$$$$$$$$$x
- .$$$$$$x+$$$$$$$$$$$$$$$$$$$$$x
- :+X$$$$$$X;$$$$$$$$$$$$$$$$$$$$$$:
- ;$$$$$$$$$$;$$$$$$$$$$$$$$$$$$$$$$X.
- +$$$$$$$$$$;x$$$$$$$$$$$$$$$$$$$$$$+
- x$$$$$$$$$$:$$$$$$$$$$$$$$$$$$$$$$X:
- .X$$$$$$$$$.:$$$$$$$$$$$$$$$$$$$$$$;
- :X$$X;;;;: .$$$$$$$$$$$$$$$$$$$$$$X.
- .$$$$X .$$$$$$$$$$$$$$$$$$$$$$$:
- .$$$$+ .X$$$$$$$$$$$$$$$$$$$$$$;
- ;$$$$: .X$$$$$$$$$$$$$$$$$$$$$$x
- :X$$$+ .$$$$$$$$$$$$$$$$$$$$$$$X
- +$$$x :$$$$$$$$$$$$$$$$$$$$$$$X
- ;$$X: $$$$$$$$$$$$$$$$$$$$$$$$X
- x$$$$$$$$$$$$$$$$$$$$$$$$X
- +$$$$$$$$$$$$$$$$$$$$$$$$$+
- .+$$$$$$$$$$$$$$$$$$$$$$$$$$;
- . ;$$$$$$$$$$$$$$$$$$$$$$$$$$$$:
- :X$x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
- .XX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+
- ;$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+$;
- .. ++X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$:+$:
- :$$+. ;$$$$$$$$$$$$$$X$$$$$$$$$$$$$$$$$$$$$;:$$+
- .x+X$X: X$$$$$$$$$$x::;:;$$$$$$$$$$$$$$$$$$X: ;$X.
- :X.x$$$:.::::::;x+:X$$$$;$$$$$$$$$$$$$$$$$$: :X;
- :x.x$$$$$$$$$$$$$$$$$;;$:$$$$$$$$$$$$$$$$$: :$+
- :Xx$$$$$$$$$$$$$$$$$: ;X;$$$$$$$$$$$$$$$$: .+$$;
- ;$$$$$$$$$$$$$$$$$$; .X+X$$$$$$$$$$$$$$$+ .+$+.
- +$$$$$$$$$$$$$$$$$$$$$$$;+$$$$$$$$$$$$$$X: .+X:
- +$$$$$$$$$$$$$$$$$$$$$$$$$+:$$$$$$$$$$$$$+.+$+.
- ;$$$$$$$$$$$$$$$$$$$$$$$$$$$X;$$$$$$$$$$$$$$X:
- +X: .:X$$$$$$$$x+++x$$$$$$$$;:X$$$$$$$$$$$X:
- :x.;$;+$$$$$:. :X$$$$X :$$$$$$$$$$X:
- ;x :X$$$; .x$$x X$$; .:+.$$$$$$$$$$x
- xx.X$$X: X$;.:$X:.X$$$$$$$$$:
- +$$$$X. ;$;::: .$$$$$$$$$:
- ;$$$; :+X$$$$XX$; X$$$$$$$$:
- ;$$X: .:x$x$$$$$X. x$$$$$$$$:
- :X$X: :+x; :$$$$$: +$$$$$$$X:
- :++$X+xXX;. +$$$$. +$$$$$$$+.
- ... .X$$$X. +$$$$$$$:
- ;$$$$; .X$$$$$$x.
- ;$$X; :X$$$$$$;
- ;$$$$$$x.
- .X$$$$$$;
- ;$$$$$$+
- +$$$$$;
- :X$$$$;.
- ;$$$$+.
- .x$$$X:
- .+$$X;
diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs
index d02a74c..2a5f0a2 100644
--- a/HellionChat/Resources/HellionStrings.Designer.cs
+++ b/HellionChat/Resources/HellionStrings.Designer.cs
@@ -451,4 +451,22 @@ internal class HellionStrings
internal static string Settings_QuickPicker_Tabs_Header => Get(nameof(Settings_QuickPicker_Tabs_Header));
internal static string Settings_ThemeAndLayout_ReduceMotion_Name => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Name));
internal static string Settings_ThemeAndLayout_ReduceMotion_Description => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Description));
+
+ // Failed-tell notification
+ internal static string FailedTell_Notification_Generic => Get(nameof(FailedTell_Notification_Generic));
+ internal static string FailedTell_Notification_Named => Get(nameof(FailedTell_Notification_Named));
+ internal static string Settings_Chat_NotifyFailedTell_Name => Get(nameof(Settings_Chat_NotifyFailedTell_Name));
+ internal static string Settings_Chat_NotifyFailedTell_Description => Get(nameof(Settings_Chat_NotifyFailedTell_Description));
+
+ // Per-tab notification sound
+ internal static string Tabs_NotificationSound_Enable_Name => Get(nameof(Tabs_NotificationSound_Enable_Name));
+ internal static string Tabs_NotificationSound_Description => Get(nameof(Tabs_NotificationSound_Description));
+ internal static string Tabs_NotificationSound_Option => Get(nameof(Tabs_NotificationSound_Option));
+ internal static string Tabs_NotificationSound_Preview => Get(nameof(Tabs_NotificationSound_Preview));
+ internal static string Tabs_NotificationSound_CustomOption => Get(nameof(Tabs_NotificationSound_CustomOption));
+
+ // Scroll-to-bottom and item/flag linking
+ internal static string ChatLog_ScrollToBottom_Tooltip => Get(nameof(ChatLog_ScrollToBottom_Tooltip));
+ internal static string ChatLog_Insert_MapFlag => Get(nameof(ChatLog_Insert_MapFlag));
+ internal static string ChatLog_Insert_ItemLink => Get(nameof(ChatLog_Insert_ItemLink));
}
diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx
index 25b64ef..79a6d45 100644
--- a/HellionChat/Resources/HellionStrings.resx
+++ b/HellionChat/Resources/HellionStrings.resx
@@ -1048,4 +1048,46 @@
Disables the theme crossfade, the sidebar and card-row hover animations, and the unread-tab pulse. Theme switches and hover states apply instantly instead.
+
+
+
+ A tell could not be delivered.
+
+
+ Tell to {0} could not be delivered.
+
+
+ Notify on failed tell
+
+
+ Show a toast when a tell you sent could not be delivered (recipient offline, in an instance, or blocking you).
+
+
+
+
+ Notification sound
+
+
+ Play a sound when a message arrives in this tab while you are looking at a different tab. Respects the global sound toggle.
+
+
+ Sound
+
+
+ Preview the selected sound
+
+
+ Hellion sound
+
+
+
+
+ Jump to the latest message
+
+
+ Insert map flag <flag>
+
+
+ Insert linked item <item>
+
diff --git a/HellionChat/Resources/Sounds/notification-1.wav b/HellionChat/Resources/Sounds/notification-1.wav
new file mode 100644
index 0000000..7e1c913
Binary files /dev/null and b/HellionChat/Resources/Sounds/notification-1.wav differ
diff --git a/HellionChat/Resources/Sounds/notification-2.wav b/HellionChat/Resources/Sounds/notification-2.wav
new file mode 100644
index 0000000..4cb6e4c
Binary files /dev/null and b/HellionChat/Resources/Sounds/notification-2.wav differ
diff --git a/HellionChat/Resources/Sounds/notification-3.wav b/HellionChat/Resources/Sounds/notification-3.wav
new file mode 100644
index 0000000..a60fbb5
Binary files /dev/null and b/HellionChat/Resources/Sounds/notification-3.wav differ
diff --git a/HellionChat/SelfTests/FoxBannerTextureSmokeStep.cs b/HellionChat/SelfTests/FoxBannerTextureSmokeStep.cs
new file mode 100644
index 0000000..45b5f87
--- /dev/null
+++ b/HellionChat/SelfTests/FoxBannerTextureSmokeStep.cs
@@ -0,0 +1,47 @@
+using Dalamud.Bindings.ImGui;
+using Dalamud.Plugin.SelfTest;
+using HellionChat.Branding;
+
+namespace HellionChat.SelfTests;
+
+// Verifies the embedded fox-banner PNG decodes into a usable texture. The load
+// is async, so the step returns Waiting until Dalamud finishes the decode and
+// the self-test runner re-polls. A decode or resource error is a build defect
+// and fails the step hard. The resource lives in the DLL, it cannot be a
+// runtime miss.
+internal sealed class FoxBannerTextureSmokeStep : ISelfTestStep
+{
+ private readonly Plugin plugin;
+
+ public FoxBannerTextureSmokeStep(Plugin plugin)
+ {
+ this.plugin = plugin;
+ }
+
+ public string Name => "Hellion Chat - Fox banner texture smoke";
+
+ public SelfTestStepResult RunStep()
+ {
+ if (!FoxBannerTexture.Shared.TryGetWrap(out var wrap, out var ex))
+ {
+ if (ex is not null)
+ {
+ ImGui.Text($"Fox banner load failed: {ex.Message}");
+ return SelfTestStepResult.Fail;
+ }
+
+ ImGui.Text("Fox banner still loading...");
+ return SelfTestStepResult.Waiting;
+ }
+
+ if (wrap.Size.X <= 0 || wrap.Size.Y <= 0)
+ {
+ ImGui.Text($"Fox banner has degenerate size {wrap.Size}");
+ return SelfTestStepResult.Fail;
+ }
+
+ return SelfTestStepResult.Pass;
+ }
+
+ public void CleanUp() { }
+}
diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs
index 1fbb088..072cb5f 100644
--- a/HellionChat/Ui/ChatLogWindow.cs
+++ b/HellionChat/Ui/ChatLogWindow.cs
@@ -14,6 +14,7 @@ using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using HellionChat.Code;
using HellionChat.GameFunctions;
using HellionChat.GameFunctions.Types;
@@ -776,6 +777,14 @@ public sealed class ChatLogWindow : Window
// (~17ms at 60fps) late, invisible inside the post-reload Atlas-Build.
private bool _firstFrameDone;
+ // Set when the user clicks the scroll-to-bottom button; the next
+ // frame's scroll-snap check forces a jump to the live end.
+ private bool _scrollToBottomRequested;
+
+ // Cached each frame inside the ##chat2-messages child. True when the
+ // user has scrolled up enough that the toolbar button should be shown.
+ private bool _childScrolledUp;
+
public override void Draw()
{
DrewThisFrame = true;
@@ -1114,6 +1123,38 @@ public sealed class ChatLogWindow : Window
using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, normalColor);
if (ImGui.Selectable(Language.ChatLog_HideChat))
UserHide();
+
+ // Insert game text-macro tokens. The game expands /- at
+ // send time, so inserting literal token text is enough. Each entry is
+ // disabled when its precondition is unmet (no map flag, no linked item)
+ // so the inserted token cannot expand to nothing.
+ unsafe
+ {
+ // Null-check before deref: pointers can be null during zone transitions.
+ var agentMap = AgentMap.Instance();
+ var flagSet = agentMap != null && agentMap->FlagMarkerCount > 0;
+ using (ImRaii.Disabled(!flagSet))
+ {
+ if (ImGui.Selectable(HellionStrings.ChatLog_Insert_MapFlag))
+ {
+ Chat += "";
+ Activate = true;
+ ActivatePos = Chat.Length;
+ }
+ }
+
+ var agentChat = AgentChatLog.Instance();
+ var itemSet = agentChat != null && agentChat->LinkedItem.ItemId != 0;
+ using (ImRaii.Disabled(!itemSet))
+ {
+ if (ImGui.Selectable(HellionStrings.ChatLog_Insert_ItemLink))
+ {
+ Chat += "
- ";
+ Activate = true;
+ ActivatePos = Chat.Length;
+ }
+ }
+ }
}
}
}
@@ -1540,17 +1581,32 @@ public sealed class ChatLogWindow : Window
Tab tab,
PayloadHandler handler,
float childHeight,
- bool switchedTab
+ bool switchedTab,
+ bool updateScrollState = true
)
{
- using var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight));
- if (!child.Success)
- return;
+ using (var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight)))
+ {
+ if (child.Success)
+ {
+ if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps)
+ DrawLogTableStyle(tab, handler, switchedTab);
+ else
+ DrawLogNormalStyle(tab, handler, switchedTab);
- if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps)
- DrawLogTableStyle(tab, handler, switchedTab);
- else
- DrawLogNormalStyle(tab, handler, switchedTab);
+ // Cached for the header toolbar's scroll-to-bottom button, which is
+ // drawn one frame later. GetScrollMaxY / GetScrollY here refer to
+ // the child's scroll context. Pop-out windows pass updateScrollState:
+ // false so they do not overwrite the main window's cached state.
+ if (updateScrollState)
+ _childScrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f;
+ }
+ else
+ {
+ if (updateScrollState)
+ _childScrolledUp = false;
+ }
+ }
}
private void DrawLogNormalStyle(Tab tab, PayloadHandler handler, bool switchedTab)
@@ -1558,8 +1614,9 @@ public sealed class ChatLogWindow : Window
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
DrawMessages(tab, handler, false);
- if (switchedTab || ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
+ if (switchedTab || _scrollToBottomRequested || ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
ImGui.SetScrollHereY(1f);
+ _scrollToBottomRequested = false;
handler.Draw();
}
@@ -1588,8 +1645,13 @@ public sealed class ChatLogWindow : Window
// Custom styles can have cellPadding that go above 4, which GetScrollY isn't respecting
var cellPaddingOffset =
!compact && oldCellPadding.Y > 4f ? oldCellPadding.Y - 4f : 0f;
- if (switchedTab || ImGui.GetScrollY() + cellPaddingOffset >= ImGui.GetScrollMaxY())
+ if (
+ switchedTab
+ || _scrollToBottomRequested
+ || ImGui.GetScrollY() + cellPaddingOffset >= ImGui.GetScrollMaxY()
+ )
ImGui.SetScrollHereY(1f);
+ _scrollToBottomRequested = false;
handler.Draw();
}
@@ -2299,14 +2361,50 @@ public sealed class ChatLogWindow : Window
Plugin.WantedTab = null;
}
- // DrawChatHeaderToolbar: renders the pop-out button for the active tab.
- // v1.3.0 also renders the optional Honorific title slot left of it.
+ // DrawChatHeaderToolbar: renders the honorific title slot, the optional
+ // scroll-to-bottom button, and the pop-out button for the active tab.
private void DrawChatHeaderToolbar(Tab tab)
{
DrawHonorificTitleSlot();
+ DrawScrollToBottomToolbarButton();
DrawPopOutButton(tab);
}
+ // Draws an arrow-down button in the toolbar when the user has scrolled up
+ // from the live end of the chat log. Clicking it requests a snap to bottom.
+ //
+ // _childScrolledUp is set at the end of DrawMessageLog, which runs AFTER
+ // DrawChatHeaderToolbar in the same frame. So this button always reflects the
+ // previous frame's scroll state, a one-frame lag that is imperceptible in use.
+ //
+ // Both this button and DrawPopOutButton use SetCursorPosX with absolute
+ // positioning (cursorX + GetContentRegionAvail().X - N * iconWidth). Because
+ // each call computes its own target X from the right edge, they are independent
+ // of each other and of what the cursor position happens to be at call time.
+ // The pop-out button lands at rightEdge - iconWidth regardless of call order.
+ private void DrawScrollToBottomToolbarButton()
+ {
+ if (!_childScrolledUp)
+ return;
+
+ var avail = ImGui.GetContentRegionAvail().X;
+ var iconWidth = ImGui.GetFrameHeight();
+ var spacing = ImGui.GetStyle().ItemSpacing.X;
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() + avail - 2 * iconWidth - spacing);
+
+ if (
+ ImGuiUtil.IconButton(
+ FontAwesomeIcon.ArrowDown,
+ tooltip: HellionStrings.ChatLog_ScrollToBottom_Tooltip
+ )
+ )
+ _scrollToBottomRequested = true;
+
+ // Keep the pop-out button on the same toolbar row. Without this the
+ // button item ends the line and the pop-out drops to the next row.
+ ImGui.SameLine();
+ }
+
private void DrawPopOutButton(Tab tab)
{
var avail = ImGui.GetContentRegionAvail().X;
@@ -2358,7 +2456,13 @@ public sealed class ChatLogWindow : Window
crownWidth = ImGui.CalcTextSize(FontAwesomeIcon.Crown.ToIconString()).X;
}
- var maxTitleWidth = avail - iconWidth - gapBeforeButton - crownWidth - gapAfterCrown;
+ // When the scroll button is also present it occupies iconWidth + ItemSpacing.X
+ // to the left of the pop-out button, so shrink the title budget accordingly.
+ var scrollButtonReserve = _childScrolledUp
+ ? iconWidth + ImGui.GetStyle().ItemSpacing.X
+ : 0f;
+ var maxTitleWidth =
+ avail - iconWidth - scrollButtonReserve - gapBeforeButton - crownWidth - gapAfterCrown;
if (maxTitleWidth <= 0)
{
return;
@@ -2484,8 +2588,13 @@ public sealed class ChatLogWindow : Window
var anyChanged = false;
var tabs = Plugin.Config.Tabs;
+ // Focus the rename field on the frame the context menu opens so the
+ // user can type immediately. Buffer raised 128 -> 512 to match the
+ // settings-tab rename (Ui/SettingsTabs/Tabs.cs). One name limit, not two.
+ if (ImGui.IsWindowAppearing())
+ ImGui.SetKeyboardFocusHere();
ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale);
- if (ImGui.InputText("##tab-name", ref tab.Name, 128))
+ if (ImGui.InputText("##tab-name", ref tab.Name, 512))
anyChanged = true;
if (ImGuiUtil.IconButton(FontAwesomeIcon.TrashAlt, tooltip: Language.ChatLog_Tabs_Delete))
diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs
index c991fc3..2247c05 100644
--- a/HellionChat/Ui/FirstRunWizard.cs
+++ b/HellionChat/Ui/FirstRunWizard.cs
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Numerics;
using Dalamud.Bindings.ImGui;
+using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using HellionChat.Branding;
@@ -171,24 +172,41 @@ public sealed class FirstRunWizard : Window
ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title);
ImGui.Spacing();
- // Banner is opt-in: the full silhouette dominates the wizard window
- // at the default size, so the TreeNode is folded by default and the
- // onboarding copy stays the primary focus. Mirrors the pre-rewrite
- // collapsible anchor from v1.5.1.
- using (var tree = ImRaii.TreeNode("Hellion Forge"))
+ // Fox-banner image: the embedded Hellion Forge fox artwork. The card
+ // behind the image gives the dark fox enough contrast against the
+ // plugin's dark UI so the logo reads clearly at a glance.
+ var banner = FoxBannerTexture.Shared.GetWrapOrDefault();
+ if (banner is not null)
{
- if (tree.Success)
- {
- using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
- {
- // CalcTextSize must run inside the MonoFont push so the
- // measurement matches the glyph width actually used for
- // rendering.
- var bannerSize = ImGui.CalcTextSize(HellionForgeAscii.FoxBanner);
- ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - bannerSize.X) * 0.5f);
- ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
- }
- }
+ const uint CardColor = 0xFFE8E8E8; // off-white fill so the dark fox pops
+ var imgHeight = 170f * ImGuiHelpers.GlobalScale;
+ var imgWidth = imgHeight * banner.Size.X / banner.Size.Y;
+ var pad = 14f * ImGuiHelpers.GlobalScale;
+ var cardWidth = imgWidth + pad * 2f;
+ var cardHeight = imgHeight + pad * 2f;
+ var rounding = 8f * ImGuiHelpers.GlobalScale;
+
+ // Centre the card in the content region. Clamp to zero so the card
+ // never shifts left of the window edge on very narrow windows.
+ var offsetX = Math.Max(0f, (ImGui.GetContentRegionAvail().X - cardWidth) * 0.5f);
+ var cardOrigin = ImGui.GetCursorScreenPos() + new Vector2(offsetX, 0f);
+
+ // Draw the rounded card behind the image, then place the image on top.
+ ImGui
+ .GetWindowDrawList()
+ .AddRectFilled(
+ cardOrigin,
+ cardOrigin + new Vector2(cardWidth, cardHeight),
+ CardColor,
+ rounding
+ );
+ ImGui.SetCursorScreenPos(cardOrigin + new Vector2(pad, pad));
+ ImGui.Image(banner.Handle, new Vector2(imgWidth, imgHeight));
+
+ // Advance the layout cursor past the full card so the content below
+ // starts at the right position and does not overlap the card.
+ ImGui.SetCursorScreenPos(cardOrigin);
+ ImGui.Dummy(new Vector2(cardWidth, cardHeight));
}
ImGui.Spacing();
diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs
index 750cc55..95c6eb4 100644
--- a/HellionChat/Ui/Popout.cs
+++ b/HellionChat/Ui/Popout.cs
@@ -118,7 +118,7 @@ internal class Popout : Window
var handler = ChatLogWindow.HandlerLender.Borrow();
var logHeight = ImGui.GetContentRegionAvail().Y - inputBarHeight - hintBannerHeight;
- ChatLogWindow.DrawMessageLog(Tab, handler, logHeight, false);
+ ChatLogWindow.DrawMessageLog(Tab, handler, logHeight, false, updateScrollState: false);
if (inputEnabled && InputBar != null)
{
diff --git a/HellionChat/Ui/SettingsTabs/Chat.cs b/HellionChat/Ui/SettingsTabs/Chat.cs
index 3115f66..d7fc96b 100644
--- a/HellionChat/Ui/SettingsTabs/Chat.cs
+++ b/HellionChat/Ui/SettingsTabs/Chat.cs
@@ -145,6 +145,12 @@ internal sealed class Chat : ISettingsTab
ref Mutable.SymbolPickerEnabled
);
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_SymbolPicker_Enable_Description);
+
+ ImGui.Checkbox(
+ HellionStrings.Settings_Chat_NotifyFailedTell_Name,
+ ref Mutable.NotifyFailedTell
+ );
+ ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyFailedTell_Description);
}
}
diff --git a/HellionChat/Ui/SettingsTabs/Information.cs b/HellionChat/Ui/SettingsTabs/Information.cs
index 9a2ba87..39c1208 100644
--- a/HellionChat/Ui/SettingsTabs/Information.cs
+++ b/HellionChat/Ui/SettingsTabs/Information.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
@@ -68,18 +69,38 @@ internal sealed class Information : ISettingsTab
DrawChangelogSection();
}
- // Provenance anchor — folded by default so the tab opens to the
- // version-info section as before. Expands to show the full Hellion
- // Forge silhouette in monospace.
private void DrawHellionForgeSection()
{
- using var tree = ImRaii.TreeNode("Hellion Forge");
- if (!tree.Success)
+ var banner = FoxBannerTexture.Shared.GetWrapOrDefault();
+ if (banner is null)
return;
- using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
- using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
- ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
+ const uint CardColor = 0xFFE8E8E8; // off-white fill so the dark fox pops
+ var imgHeight = 170f * ImGuiHelpers.GlobalScale;
+ var imgWidth = imgHeight * banner.Size.X / banner.Size.Y;
+ var pad = 14f * ImGuiHelpers.GlobalScale;
+ var cardWidth = imgWidth + pad * 2f;
+ var cardHeight = imgHeight + pad * 2f;
+ var rounding = 8f * ImGuiHelpers.GlobalScale;
+
+ // Left-aligned: card origin stays at the current layout cursor position.
+ var cardOrigin = ImGui.GetCursorScreenPos();
+
+ // Draw the rounded card behind the image, then place the image on top.
+ ImGui
+ .GetWindowDrawList()
+ .AddRectFilled(
+ cardOrigin,
+ cardOrigin + new Vector2(cardWidth, cardHeight),
+ CardColor,
+ rounding
+ );
+ ImGui.SetCursorScreenPos(cardOrigin + new Vector2(pad, pad));
+ ImGui.Image(banner.Handle, new Vector2(imgWidth, imgHeight));
+
+ // Advance the layout cursor past the full card so content below does not overlap.
+ ImGui.SetCursorScreenPos(cardOrigin);
+ ImGui.Dummy(new Vector2(cardWidth, cardHeight));
}
private void DrawVersionInfoSection()
diff --git a/HellionChat/Ui/SettingsTabs/Tabs.cs b/HellionChat/Ui/SettingsTabs/Tabs.cs
index 3f0948b..2d97523 100755
--- a/HellionChat/Ui/SettingsTabs/Tabs.cs
+++ b/HellionChat/Ui/SettingsTabs/Tabs.cs
@@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
+using FFXIVClientStructs.FFXIV.Client.UI;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
@@ -165,6 +166,78 @@ internal sealed class Tabs : ISettingsTab
}
ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp);
+ ImGui.Checkbox(
+ HellionStrings.Tabs_NotificationSound_Enable_Name,
+ ref tab.EnableNotificationSound
+ );
+ ImGuiUtil.HelpMarker(HellionStrings.Tabs_NotificationSound_Description);
+ if (tab.EnableNotificationSound)
+ {
+ using var indent = ImRaii.PushIndent(10.0f);
+ // Build a readable preview label for the currently selected sound.
+ var soundPreview =
+ tab.NotificationSoundId <= 16
+ ? $"{HellionStrings.Tabs_NotificationSound_Option} {tab.NotificationSoundId}"
+ : $"{HellionStrings.Tabs_NotificationSound_CustomOption} {tab.NotificationSoundId - 16}";
+ using (var combo = ImRaii.Combo($"##notif-sound-{i}", soundPreview))
+ {
+ if (combo.Success)
+ {
+ for (uint s = 1; s <= 16; s++)
+ {
+ if (
+ ImGui.Selectable(
+ $"{HellionStrings.Tabs_NotificationSound_Option} {s}",
+ tab.NotificationSoundId == s
+ )
+ )
+ tab.NotificationSoundId = s;
+ }
+
+ ImGui.Separator();
+
+ // Bundled custom sounds (ids 17-19).
+ for (uint n = 1; n <= 3; n++)
+ {
+ var customId = 16 + n;
+ if (
+ ImGui.Selectable(
+ $"{HellionStrings.Tabs_NotificationSound_CustomOption} {n}",
+ tab.NotificationSoundId == customId
+ )
+ )
+ tab.NotificationSoundId = customId;
+ }
+ }
+ }
+
+ // Let the user hear the currently selected sound without waiting
+ // for a real message to arrive in this tab.
+ ImGui.SameLine();
+ if (
+ ImGuiUtil.IconButton(
+ FontAwesomeIcon.Play,
+ tooltip: HellionStrings.Tabs_NotificationSound_Preview
+ )
+ )
+ {
+ var previewId = tab.NotificationSoundId;
+ if (previewId <= 16)
+ {
+ Plugin.Framework.RunOnFrameworkThread(() =>
+ {
+ unsafe
+ {
+ UIGlobals.PlaySoundEffect(previewId);
+ }
+ });
+ }
+ else
+ {
+ Plugin.CustomAudioPlayer.Play((int)previewId - 16);
+ }
+ }
+ }
ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut);
if (tab.PopOut)
{
diff --git a/HellionChat/_Helpers/FailedTellMatcher.cs b/HellionChat/_Helpers/FailedTellMatcher.cs
new file mode 100644
index 0000000..5fbb5c0
--- /dev/null
+++ b/HellionChat/_Helpers/FailedTellMatcher.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+
+namespace HellionChat._Helpers;
+
+// Pure decision helper for failed-tell detection. The processed message
+// stream carries no LogMessage row id, so detection happens at the
+// RaptureLogModule level (see FailedTellNotifier). This POCO stays free of
+// Dalamud types so the "known id AND enabled" rule is Build-Suite testable.
+// TEST-MIRROR: ../../../Hellion Build test/Ui/FailedTellMatcherTests.cs
+public static class FailedTellMatcher
+{
+ // Log-message ids the game raises for a tell that could not be delivered,
+ // pinned from in-game discovery. 50 covers an unreachable recipient
+ // (offline, non-existent, or on another world); 3832 is a recipient
+ // inside an instance.
+ public static readonly IReadOnlySet FailedTellLogMessageIds = new HashSet
+ {
+ 50u,
+ 3832u,
+ };
+
+ public static bool ShouldNotify(
+ uint logMessageId,
+ bool notifyEnabled,
+ IReadOnlySet failedTellIds
+ ) => notifyEnabled && failedTellIds.Contains(logMessageId);
+}
diff --git a/HellionChat/_Helpers/TabSoundDecision.cs b/HellionChat/_Helpers/TabSoundDecision.cs
new file mode 100644
index 0000000..87055b4
--- /dev/null
+++ b/HellionChat/_Helpers/TabSoundDecision.cs
@@ -0,0 +1,17 @@
+namespace HellionChat._Helpers;
+
+// Pure decision helper: should an incoming message play a per-tab notification
+// sound? Kept Dalamud-free so the Build Suite can test the
+// "inactive + enabled + global-allowed" rule in isolation.
+// TEST-MIRROR: ../../../Hellion Build test/Ui/TabSoundDecisionTests.cs
+public static class TabSoundDecision
+{
+ // True only when the message landed in a tab the user is not looking at,
+ // that tab has its own sound switched on, and the global sound master is
+ // not muted.
+ public static bool ShouldPlay(
+ bool isActiveTab,
+ bool tabSoundEnabled,
+ bool globalSoundsEnabled
+ ) => !isActiveTab && tabSoundEnabled && globalSoundsEnabled;
+}
diff --git a/HellionChat/packages.lock.json b/HellionChat/packages.lock.json
index b50ef46..acf75ef 100644
--- a/HellionChat/packages.lock.json
+++ b/HellionChat/packages.lock.json
@@ -102,6 +102,15 @@
"resolved": "4.4.0",
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
},
+ "NAudio.WinMM": {
+ "type": "Direct",
+ "requested": "[2.2.1, )",
+ "resolved": "2.2.1",
+ "contentHash": "xFHRFwH4x6aq3IxRbewvO33ugJRvZFEOfO62i7uQJRUNW2cnu6BeBTHUS0JD5KBucZbHZaYqxQG8dwZ47ezQuQ==",
+ "dependencies": {
+ "NAudio.Core": "2.2.1"
+ }
+ },
"Pidgin": {
"type": "Direct",
"requested": "[3.5.1, 4.0.0)",
@@ -366,6 +375,11 @@
"resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
},
+ "NAudio.Core": {
+ "type": "Transitive",
+ "resolved": "2.2.1",
+ "contentHash": "GgkdP6K/7FqXFo7uHvoqGZTJvW4z8g2IffhOO4JHaLzKCdDOUEzVKtveoZkCuUX8eV2HAINqi7VFqlFndrnz/g=="
+ },
"SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
diff --git a/README.md b/README.md
index 76ea8bb..77b8b05 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
[](LICENSE)
-[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
+[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
[](https://github.com/goatcorp/Dalamud)
[](https://dotnet.microsoft.com/)
[](https://www.finalfantasyxiv.com/)
@@ -11,7 +11,7 @@
-**Version 1.5.4** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
+**Version 1.5.5** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine
@@ -299,17 +299,16 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
## Project Status
-**Version 1.5.4** — Polish and Motion. Theme switches now crossfade smoothly over
-~300 ms across every Hellion-rendered surface — sidebar, title, buttons, tabs,
-scrollbar, separators. The window background snaps deliberately so the per-window
-opacity override from Dalamud's pinning menu stays intact. A new header quick-picker —
-a palette button left of the cog — opens a compact popup that switches themes and tabs
-without opening Settings; the active entry carries a check glyph and the popup stays
-open between picks. Sidebar icons ease their opacity on hover and card-mode message
-borders highlight per tab, both framerate-independent so a stalled Wine frame cannot
-overshoot. A new "Reduce motion" toggle in Theme & Layout disables the crossfade, the
-hover animations and the unread-tab pulse for users who prefer a static UI. No schema
-bump, migration v17 stays.
+**Version 1.5.5** — Upstream-Sync Tab-Features. Failed tells now raise a warning toast
+when a message could not be delivered (recipient offline, in an instance, or blocking
+you). Per-tab notification sounds let each tab play one of the 16 game chat sounds or
+three bundled Hellion sounds when a message arrives on a background tab, with a
+preview button. The tab rename field in the right-click menu auto-focuses on open and
+accepts up to 512 characters. A jump-to-latest button appears in the chat log header
+while scrolled up from the live end. Map-flag and item-link insertion is available from
+the chat input right-click menu. The Hellion Forge fox banner in the first-run wizard
+and the Information tab is now a real image. Schema bumped to v18, additive fields
+only, no data migration.
---
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 2f1056c..5f0b965 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -11,6 +11,17 @@ releases as an overview and links to the release pages for details.
---
+## Hellion Chat 1.5.5 — Upstream-Sync Tab-Features (2026-05-21)
+
+A backlog-sync cycle. Inherited tab items: a failed-tell warning toast, per-tab
+notification sounds (16 game sounds or three bundled Hellion sounds with a
+preview button), an auto-focusing 512-character tab rename, a jump-to-latest
+button in the chat log header, and map-flag / item-link insertion from the chat
+input. Plus the Hellion Forge fox banner becomes a real image. Schema bumped to
+v18, additive fields only, no data migration.
+
+---
+
## Hellion Chat 1.5.4 — Polish and Motion (2026-05-20)
A polish cycle of three P3 items. Theme switches now crossfade smoothly over ~300 ms
diff --git a/docs/CREDITS.md b/docs/CREDITS.md
new file mode 100644
index 0000000..5fa9f4e
--- /dev/null
+++ b/docs/CREDITS.md
@@ -0,0 +1,20 @@
+# Third-Party Asset Credits
+
+## Bundled Notification Sounds
+
+- **notification-1.wav, notification-2.wav, notification-3.wav**
+ Creator: Universfield
+ Source: [Pixabay](https://pixabay.com/users/universfield-28281460/)
+ License: Pixabay Content License (royalty-free, commercial use permitted)
+
+## Branding
+
+- **Fox banner image** (`fox-banner.png`)
+ Creator: InklyTattooDesigns (Etsy)
+ License: Royalty-free for commercial and logo use
+
+## Libraries
+
+- **NAudio** - Copyright (c) Mark Heath
+ License: MIT
+ Source:
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 1a1acaa..fbd3e09 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -12,16 +12,30 @@ be a poor fit for the plugin's privacy-first scope during brainstorming.
## Next Cycle
-**Plugin Integrations Wave 2-6** (Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM
-Quick-DM) is the next planned scope. The UiBuilder first-frame HITCH investigation that v1.5.1
-queued is now closed as a side effect of v1.5.3's font-stack fix — HITCH dropped from ~74 ms into
-the 15-25 ms range. The Wine/Linux scroll-rubber-band spike remains at the tail.
+**v1.5.5b — Upstream-Sync Filter/Notification/Polish** is the next planned scope: the second half
+of the backlog-sync wave, covering filter improvements, notification refinements, and UI polish
+items that did not fit the v1.5.5 bundle. Plugin Integrations Wave 2-6 (Context-Menu,
+NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM) follows.
Native-speaker review of the AI-assisted v1.5.3 translations (13 legacy Crowdin locales) runs in
parallel as a continuous correction pass, gathered via the Hellion Forge Discord.
---
+## v1.5.5 — Upstream-Sync Tab-Features (released 2026-05-21)
+
+A backlog-sync cycle of inherited tab-feature items. Failed tells now raise a warning toast when a
+message could not be delivered (recipient offline, in an instance, or blocking you), toggleable in
+Settings. Per-tab notification sounds let each tab play one of the 16 game chat sounds or three
+bundled Hellion sounds when a message arrives on a background tab, with a preview button. The tab
+rename field in the right-click menu auto-focuses on open and accepts up to 512 characters. A
+jump-to-latest button appears in the chat log header while scrolled up from the live end.
+Map-flag and item-link insertion is available from the chat input right-click menu. The Hellion
+Forge fox banner in the first-run wizard and the Information tab is now a real image instead of
+ASCII art. Schema bumped to v18, additive fields only, no data migration.
+
+---
+
## v1.5.4 — Polish and Motion (released 2026-05-20)
A polish cycle of three P3 items. Theme switches crossfade over ~300 ms across every
diff --git a/repo.json b/repo.json
index e32f36f..68a6f5c 100644
--- a/repo.json
+++ b/repo.json
@@ -3,7 +3,7 @@
"Author": "Jon Kazama (Hellion Forge)",
"Name": "Hellion Chat",
"InternalName": "HellionChat",
- "AssemblyVersion": "1.5.4.0",
+ "AssemblyVersion": "1.5.5.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 four profiles: Privacy-First, Casual, Roleplay, Full History\n- Multi-language UI (24 locales) 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",
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
@@ -20,12 +20,12 @@
"CanUnloadAsync": false,
"LoadPriority": 0,
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
- "Changelog": "**v1.5.4 — Polish and Motion (2026-05-20)**\n\nA polish cycle: smoother theme switching, faster theme and tab\naccess, and subtle hover motion. Three P3 items plus an\naccessibility toggle.\n\nUser-visible:\n\n- Theme switches now crossfade smoothly over ~300 ms across every\n Hellion-rendered surface — sidebar, title, buttons, tabs,\n scrollbar, separators. The window background snaps deliberately\n so the per-window opacity override from Dalamud's pinning menu\n stays untouched.\n- New header quick-picker: a palette button left of the cog opens\n a compact popup with two sections — every built-in and custom\n theme, and every tab. The active entry carries a check glyph;\n clicking another switches without closing the popup.\n- Sidebar icons ease their opacity on hover, and card-mode message\n borders highlight per tab while the cursor is over their rows.\n Framerate-independent, so a stalled Wine frame cannot overshoot\n the animation.\n- New \"Reduce motion\" toggle in Theme & Layout disables the\n crossfade, the hover animations and the unread-tab pulse for\n users who prefer a static UI.\n\nUnder the hood:\n\n- Two pure-helper lerp paths (ThemeAbgrCacheLerp, FrameLerp) with\n xUnit coverage in the Build Suite, plus a ColourUtil.ApplyAlpha\n alpha modulator. Two new /xlperf self-test steps pin the\n crossfade and quick-picker contracts.\n\nNo schema bump, no migration. Migration v17 stays.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.3 — Localisation Wave + Bundled-Font Overhaul (2026-05-19)**\n\nMulti-language pass plus a long-standing first-frame HITCH lands\nas a side effect of a font-stack rewrite.\n\nUser-visible:\n\n- 24 selectable UI languages (was 2). Catalan, Czech, Danish,\n Dutch, English, Finnish, French, German, Greek, Hungarian,\n Italian, Japanese, Korean, Norsk bokmål, Polish, Portuguese\n (BR + PT), Romanian, Russian, Spanish, Swedish, Turkish,\n Ukrainian, Simplified + Traditional Chinese. Sorted by endonym,\n \"None\" pinned first. Non-native locales are AI-assisted and\n flagged for native-speaker review via the Forge Discord.\n- Bundled Inter Light replaces Exo 2 (SIL OFL 1.1, 343 KB). The\n Inter font ships Latin Extended-A/B, Greek polytonic and\n Cyrillic Supplement coverage; NotoSansCjkRegular joins as a\n third merge layer for Hangul and Simplified-Han glyphs the\n FFXIV Japanese game font does not ship.\n- First-frame HITCH dropped from ~74 ms (v1.5.2 baseline that\n held since v1.4.x) to a median of ~20 ms (5-reload sample\n 17.9-23.6 ms, Linux/Wine). The bundled-font path silently\n fell back to the FFXIV Axis font for the entire v1.5.x series\n because of an early-return in the draw loop. The fix that\n routes RegularFont through draw also lands the defer-pattern\n win the v1.5.1 cycle was reaching for.\n- ExtraGlyphRanges auto-activates on language change. Korean,\n ChineseFull and the two new flags (LatinExtended, Greek) toggle\n on without a manual visit to Fonts and Colours.\n- New WarningText under the language dropdown notes FFXIV's\n chat input only fully supports EN/DE/FR/JA character sets.\n Other languages render in HellionChat but may garble when\n typed into in-game chat.\n\nUnder the hood:\n\n- Three-layer font stack: Inter Light primary, FFXIV\n JapaneseFont merge 1 for kana/kanji style, NotoSansCjkRegular\n merge 2 for everything else CJK.\n- LanguageOverride enum gains ten locales plus three previously\n commented out (Italian, Korean, Norwegian as `nb`). New\n values append to the enum so existing config integers stay\n stable across update.\n- Crowdin gap closed: four post-sync ChatTwo keys backfilled\n into 13 legacy locales with per-key AI markers.\n- Plugin.LoadAsync runs a one-shot migration that ORs in the\n matching ExtraGlyphRanges flag for users already on a\n non-default language. Settings.Apply auto-activates on\n change going forward.\n- Em-dash sweep across the EN source and 18 translations to the\n house style. Russian and Ukrainian keep the typographic norm.\n\nMigration v17 stays. UseHellionFont users transition from Exo 2\nto Inter Light transparently on first reload.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.2 — First-Run Wizard Rework (2026-05-18)**\n\nUX patch. The first-run wizard becomes a four-step flow with a\nnew Roleplay privacy profile and a power-settings step that\nsurfaces previously-hidden defaults. Existing v1.5.1 users see\nthe new wizard once on first v1.5.2 boot.\n\nWhat changes user-visible:\n\n- Wizard navigation: Welcome → Privacy profile → Power settings\n → Done. Forge-Bronze pagination dots, dedicated stage for the\n power settings so they are no longer buried in Settings.\n- Fourth privacy profile \"Roleplay\": Privacy-First plus Say and\n both emote types, with a 30-day window for Say and a 90-day\n window for emotes. Shout, Yell and Novice Network stay out.\n- Privacy picker becomes a 2x2 grid. Casual stays the\n recommended option with a ★ marker.\n- Power-settings step covers Load Previous Session, Filter\n Include Previous Sessions, Auto-Tell-Tabs History Preload,\n Compact Density, Prettier Timestamps and a built-in theme\n picker. All six map to existing Configuration fields — no new\n settings introduced.\n- Staged commit: the wizard only writes to Config on the Finish\n step. Decide-later or X-close at any point leaves the existing\n config untouched.\n- Inline test hint on the done step: \"type /tell \n into chat\" surfaces the auto-tell-tab spawn mechanism.\n- Window starts at 720x480 (was 900x560) and can shrink to\n 600x400; Step 1 keeps the fox banner in a folded TreeNode so\n the onboarding copy stays primary.\n- Existing users get the new wizard surfaced once on first boot\n after the update via the new WizardLastShownVersion config\n field. Future cycles bump the constant only when the wizard\n itself changes shape.\n\nUnder the hood:\n\n- WizardStateSmokeStep added to /xlperf alongside the FontManager\n and ThemeSwitch self-tests.\n- Twelve new pure-helper xUnit Facts in the Build Suite cover\n all four privacy profile sets and their retention overrides.\n\nMigration v17 stays (no schema bump). The Configuration grows\none optional string field (WizardLastShownVersion) which\ndefaults to empty for legacy users.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**\n\nHybrid FontManager refactor plus an embedded provenance mark.\n\nWhat changes under the hood:\n\n- FontManager handle creation moves into the ctor inside a single\n atlas.SuppressAutoRebuild() block. The font atlas now builds once\n per plugin load instead of four to five times — less CPU and GPU\n pressure in the first seconds after a reload, less atlas texture\n memory churn.\n- Hybrid property model: Axis, AxisItalic and FontAwesome become\n init-only handles. RegularFont and ItalicFont stay mutable because\n the eight font settings still need to replace them at runtime —\n that path is funnelled through RebuildDelegateFonts() now and\n runs without a plugin reload.\n- FontAwesome reuses Dalamud's UiBuilder.IconFontFixedWidthHandle\n instead of building its own atlas slot. One delegate-build step\n less in the ctor.\n- BuildFontsAsync and BuildFonts are removed; the live mutation\n path is RebuildDelegateFonts() now.\n- Two FontManager self-test steps registered with /xlperf: ctor\n smoke (every handle non-null after Phase-1 resolve, no atlas\n load-exception) and push smoke (Push() returns without throwing).\n\nHonorific full-gradient port (originally the v1.5.1 main item) was\ndropped: Honorific 3.2 exposes no IPC for the rendered gradient\nframe, and an in-plugin port of the colour palette was declined.\nThe integration stays at the v1.4.7 glow-only shape.\n\nUser-visible:\n\n- Hellion Forge signature: a small fox-head ASCII silhouette is\n emitted to /xllog on every plugin load, and a full fox banner\n with \"Hellion Forge\" set inside the body is available as a\n folded TreeNode in the First-Run Wizard and Settings ->\n Information tab. Drawn by Julia Moon, embedded in the plugin DLL.\n- No settings changes, no migration. v17 stays.\n\nNote on performance: the cross-plugin baseline target from v1.5.0\n(matching Lightless and XIVInstantMessenger at ~7 ms HITCH) did\nnot land this cycle. HITCH stays around 80 ms because the cost is\nin the UiBuilder first-frame render path, not in the atlas build\n(which this cycle did reduce from 4-5 builds per load to 1). A\nfirst-frame render investigation is reserved for a later cycle.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
+ "Changelog": "**v1.5.5 — Upstream-Sync Tab-Features (2026-05-21)**\n\nA backlog-sync cycle: inherited tab-feature items plus a new fox\nbanner image and custom notification sounds.\n\nUser-visible:\n\n- Failed tells now raise a warning toast when a message you sent\n could not be delivered (recipient offline, in an instance, or\n blocking you). Toggle in Settings, Chat tab.\n- Per-tab notification sound: each tab can play a sound when a\n message arrives while you are looking at a different tab. Pick\n one of the 16 game chat sounds or one of three bundled Hellion\n sounds, with a preview button to hear it. Off by default,\n respects the global sound toggle.\n- The tab rename field in the right-click menu now focuses\n itself when the menu opens and accepts up to 512 characters,\n matching the settings-tab rename.\n- A jump-to-latest button appears in the chat log header while\n you are scrolled up from the live end.\n- Map flags and item links can be inserted into the chat input\n from its right-click menu.\n- The Hellion Forge fox banner in the first-run wizard and the\n Information tab is now a real image instead of ASCII art.\n\nSchema bumped to v18 (additive fields only, no data migration).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.4 — Polish and Motion (2026-05-20)**\n\nA polish cycle: smoother theme switching, faster theme and tab\naccess, and subtle hover motion. Three P3 items plus an\naccessibility toggle.\n\nUser-visible:\n\n- Theme switches now crossfade smoothly over ~300 ms across every\n Hellion-rendered surface — sidebar, title, buttons, tabs,\n scrollbar, separators. The window background snaps deliberately\n so the per-window opacity override from Dalamud's pinning menu\n stays untouched.\n- New header quick-picker: a palette button left of the cog opens\n a compact popup with two sections — every built-in and custom\n theme, and every tab. The active entry carries a check glyph;\n clicking another switches without closing the popup.\n- Sidebar icons ease their opacity on hover, and card-mode message\n borders highlight per tab while the cursor is over their rows.\n Framerate-independent, so a stalled Wine frame cannot overshoot\n the animation.\n- New \"Reduce motion\" toggle in Theme & Layout disables the\n crossfade, the hover animations and the unread-tab pulse for\n users who prefer a static UI.\n\nUnder the hood:\n\n- Two pure-helper lerp paths (ThemeAbgrCacheLerp, FrameLerp) with\n xUnit coverage in the Build Suite, plus a ColourUtil.ApplyAlpha\n alpha modulator. Two new /xlperf self-test steps pin the\n crossfade and quick-picker contracts.\n\nNo schema bump, no migration. Migration v17 stays.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.3 — Localisation Wave + Bundled-Font Overhaul (2026-05-19)**\n\nMulti-language pass plus a long-standing first-frame HITCH lands\nas a side effect of a font-stack rewrite.\n\nUser-visible:\n\n- 24 selectable UI languages (was 2). Catalan, Czech, Danish,\n Dutch, English, Finnish, French, German, Greek, Hungarian,\n Italian, Japanese, Korean, Norsk bokmål, Polish, Portuguese\n (BR + PT), Romanian, Russian, Spanish, Swedish, Turkish,\n Ukrainian, Simplified + Traditional Chinese. Sorted by endonym,\n \"None\" pinned first. Non-native locales are AI-assisted and\n flagged for native-speaker review via the Forge Discord.\n- Bundled Inter Light replaces Exo 2 (SIL OFL 1.1, 343 KB). The\n Inter font ships Latin Extended-A/B, Greek polytonic and\n Cyrillic Supplement coverage; NotoSansCjkRegular joins as a\n third merge layer for Hangul and Simplified-Han glyphs the\n FFXIV Japanese game font does not ship.\n- First-frame HITCH dropped from ~74 ms (v1.5.2 baseline that\n held since v1.4.x) to a median of ~20 ms (5-reload sample\n 17.9-23.6 ms, Linux/Wine). The bundled-font path silently\n fell back to the FFXIV Axis font for the entire v1.5.x series\n because of an early-return in the draw loop. The fix that\n routes RegularFont through draw also lands the defer-pattern\n win the v1.5.1 cycle was reaching for.\n- ExtraGlyphRanges auto-activates on language change. Korean,\n ChineseFull and the two new flags (LatinExtended, Greek) toggle\n on without a manual visit to Fonts and Colours.\n- New WarningText under the language dropdown notes FFXIV's\n chat input only fully supports EN/DE/FR/JA character sets.\n Other languages render in HellionChat but may garble when\n typed into in-game chat.\n\nUnder the hood:\n\n- Three-layer font stack: Inter Light primary, FFXIV\n JapaneseFont merge 1 for kana/kanji style, NotoSansCjkRegular\n merge 2 for everything else CJK.\n- LanguageOverride enum gains ten locales plus three previously\n commented out (Italian, Korean, Norwegian as `nb`). New\n values append to the enum so existing config integers stay\n stable across update.\n- Crowdin gap closed: four post-sync ChatTwo keys backfilled\n into 13 legacy locales with per-key AI markers.\n- Plugin.LoadAsync runs a one-shot migration that ORs in the\n matching ExtraGlyphRanges flag for users already on a\n non-default language. Settings.Apply auto-activates on\n change going forward.\n- Em-dash sweep across the EN source and 18 translations to the\n house style. Russian and Ukrainian keep the typographic norm.\n\nMigration v17 stays. UseHellionFont users transition from Exo 2\nto Inter Light transparently on first reload.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.2 — First-Run Wizard Rework (2026-05-18)**\n\nUX patch. The first-run wizard becomes a four-step flow with a\nnew Roleplay privacy profile and a power-settings step that\nsurfaces previously-hidden defaults. Existing v1.5.1 users see\nthe new wizard once on first v1.5.2 boot.\n\nWhat changes user-visible:\n\n- Wizard navigation: Welcome → Privacy profile → Power settings\n → Done. Forge-Bronze pagination dots, dedicated stage for the\n power settings so they are no longer buried in Settings.\n- Fourth privacy profile \"Roleplay\": Privacy-First plus Say and\n both emote types, with a 30-day window for Say and a 90-day\n window for emotes. Shout, Yell and Novice Network stay out.\n- Privacy picker becomes a 2x2 grid. Casual stays the\n recommended option with a ★ marker.\n- Power-settings step covers Load Previous Session, Filter\n Include Previous Sessions, Auto-Tell-Tabs History Preload,\n Compact Density, Prettier Timestamps and a built-in theme\n picker. All six map to existing Configuration fields — no new\n settings introduced.\n- Staged commit: the wizard only writes to Config on the Finish\n step. Decide-later or X-close at any point leaves the existing\n config untouched.\n- Inline test hint on the done step: \"type /tell \n into chat\" surfaces the auto-tell-tab spawn mechanism.\n- Window starts at 720x480 (was 900x560) and can shrink to\n 600x400; Step 1 keeps the fox banner in a folded TreeNode so\n the onboarding copy stays primary.\n- Existing users get the new wizard surfaced once on first boot\n after the update via the new WizardLastShownVersion config\n field. Future cycles bump the constant only when the wizard\n itself changes shape.\n\nUnder the hood:\n\n- WizardStateSmokeStep added to /xlperf alongside the FontManager\n and ThemeSwitch self-tests.\n- Twelve new pure-helper xUnit Facts in the Build Suite cover\n all four privacy profile sets and their retention overrides.\n\nMigration v17 stays (no schema bump). The Configuration grows\none optional string field (WizardLastShownVersion) which\ndefaults to empty for legacy users.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
"AcceptsFeedback": true,
- "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.4/latest.zip",
- "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.4/latest.zip",
- "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.4/latest.zip",
- "TestingAssemblyVersion": "1.5.4.0",
+ "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.5/latest.zip",
+ "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.5/latest.zip",
+ "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.5/latest.zip",
+ "TestingAssemblyVersion": "1.5.5.0",
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
"ImageUrls": [
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",