Merge branch 'feature/v1.5.5'
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||
<PropertyGroup>
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.5.4</Version>
|
||||
<Version>1.5.5</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
@@ -26,6 +26,11 @@
|
||||
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||
<!-- NAudio.WinMM 2.2.1 MIT - WaveOutEvent/WinMM path is Wine-safe (WaveOut works under Wine,
|
||||
Media-Foundation-based codecs do not). Using the sub-package avoids pulling in
|
||||
NAudio.WinForms (which requires WindowsDesktop and does not build on Linux hosts).
|
||||
WaveOutEvent and WaveFileReader both live in NAudio.WinMM + NAudio.Core. -->
|
||||
<PackageReference Include="NAudio.WinMM" Version="2.2.1" />
|
||||
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4.0.0)" />
|
||||
</ItemGroup>
|
||||
@@ -58,8 +63,18 @@
|
||||
<EmbeddedResource Include="Resources\Inter-OFL.txt">
|
||||
<LogicalName>Inter-OFL.txt</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Branding\fox-banner.txt">
|
||||
<LogicalName>HellionChat.Branding.fox-banner.txt</LogicalName>
|
||||
<EmbeddedResource Include="Resources\Branding\fox-banner.png">
|
||||
<LogicalName>HellionChat.Branding.fox-banner.png</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<!-- Bundled custom notification sounds, Mono 44.1 kHz 16-bit PCM WAV (Wine-safe) -->
|
||||
<EmbeddedResource Include="Resources\Sounds\notification-1.wav">
|
||||
<LogicalName>HellionChat.Sounds.notification-1.wav</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Sounds\notification-2.wav">
|
||||
<LogicalName>HellionChat.Sounds.notification-2.wav</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Sounds\notification-3.wav">
|
||||
<LogicalName>HellionChat.Sounds.notification-3.wav</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Branding\fox-mini.txt">
|
||||
<LogicalName>HellionChat.Branding.fox-mini.txt</LogicalName>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<CustomAudioPlayer> _logger;
|
||||
|
||||
private WaveOutEvent? _outputDevice;
|
||||
private WaveFileReader? _reader;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public CustomAudioPlayer(ILogger<CustomAudioPlayer> 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<byte>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<FailedTellNotifier> _logger;
|
||||
private readonly Hook<RaptureLogModule.Delegates.ShowLogMessageString>? _hook;
|
||||
|
||||
public unsafe FailedTellNotifier(ILogger<FailedTellNotifier> 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.Delegates.ShowLogMessageString>(
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+10
-5
@@ -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<TypingIpc>();
|
||||
ExtraChat = _host.Services.GetRequiredService<ExtraChat>();
|
||||
HonorificService = _host.Services.GetRequiredService<Integrations.HonorificService>();
|
||||
CustomAudioPlayer = _host.Services.GetRequiredService<Integrations.CustomAudioPlayer>();
|
||||
StatusBar = _host.Services.GetRequiredService<Ui.StatusBar>();
|
||||
MessageManager = _host.Services.GetRequiredService<MessageManager>();
|
||||
AutoTellTabsService = _host.Services.GetRequiredService<AutoTellTabsService>();
|
||||
@@ -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
|
||||
|
||||
@@ -107,6 +107,12 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
|
||||
sp.GetRequiredService<IFramework>()
|
||||
));
|
||||
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
||||
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
||||
));
|
||||
services.AddSingleton(sp => new Integrations.CustomAudioPlayer(
|
||||
sp.GetRequiredService<ILogger<Integrations.CustomAudioPlayer>>()
|
||||
));
|
||||
|
||||
services.AddSingleton(sp => new MessageManager(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
@@ -172,6 +178,11 @@ internal static class PluginHostFactory
|
||||
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||
sp.GetRequiredService<AutoTellTabsService>()
|
||||
));
|
||||
services.AddHostedService(
|
||||
sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
|
||||
sp.GetRequiredService<Integrations.FailedTellNotifier>()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 419 KiB |
@@ -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;
|
||||
+18
@@ -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));
|
||||
}
|
||||
|
||||
@@ -1048,4 +1048,46 @@
|
||||
<data name="Settings_ThemeAndLayout_ReduceMotion_Description" xml:space="preserve">
|
||||
<value>Disables the theme crossfade, the sidebar and card-row hover animations, and the unread-tab pulse. Theme switches and hover states apply instantly instead.</value>
|
||||
</data>
|
||||
|
||||
<!-- Failed-tell notification -->
|
||||
<data name="FailedTell_Notification_Generic" xml:space="preserve">
|
||||
<value>A tell could not be delivered.</value>
|
||||
</data>
|
||||
<data name="FailedTell_Notification_Named" xml:space="preserve">
|
||||
<value>Tell to {0} could not be delivered.</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_NotifyFailedTell_Name" xml:space="preserve">
|
||||
<value>Notify on failed tell</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_NotifyFailedTell_Description" xml:space="preserve">
|
||||
<value>Show a toast when a tell you sent could not be delivered (recipient offline, in an instance, or blocking you).</value>
|
||||
</data>
|
||||
|
||||
<!-- Per-tab notification sound -->
|
||||
<data name="Tabs_NotificationSound_Enable_Name" xml:space="preserve">
|
||||
<value>Notification sound</value>
|
||||
</data>
|
||||
<data name="Tabs_NotificationSound_Description" xml:space="preserve">
|
||||
<value>Play a sound when a message arrives in this tab while you are looking at a different tab. Respects the global sound toggle.</value>
|
||||
</data>
|
||||
<data name="Tabs_NotificationSound_Option" xml:space="preserve">
|
||||
<value>Sound</value>
|
||||
</data>
|
||||
<data name="Tabs_NotificationSound_Preview" xml:space="preserve">
|
||||
<value>Preview the selected sound</value>
|
||||
</data>
|
||||
<data name="Tabs_NotificationSound_CustomOption" xml:space="preserve">
|
||||
<value>Hellion sound</value>
|
||||
</data>
|
||||
|
||||
<!-- Scroll-to-bottom and item/flag linking -->
|
||||
<data name="ChatLog_ScrollToBottom_Tooltip" xml:space="preserve">
|
||||
<value>Jump to the latest message</value>
|
||||
</data>
|
||||
<data name="ChatLog_Insert_MapFlag" xml:space="preserve">
|
||||
<value>Insert map flag <flag></value>
|
||||
</data>
|
||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||
<value>Insert linked item <item></value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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() { }
|
||||
}
|
||||
+123
-14
@@ -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 <flag>/<item> 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 += "<flag>";
|
||||
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 += "<item>";
|
||||
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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<uint> FailedTellLogMessageIds = new HashSet<uint>
|
||||
{
|
||||
50u,
|
||||
3832u,
|
||||
};
|
||||
|
||||
public static bool ShouldNotify(
|
||||
uint logMessageId,
|
||||
bool notifyEnabled,
|
||||
IReadOnlySet<uint> failedTellIds
|
||||
) => notifyEnabled && failedTellIds.Contains(logMessageId);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://github.com/goatcorp/Dalamud)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://www.finalfantasyxiv.com/)
|
||||
@@ -11,7 +11,7 @@
|
||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||
</p>
|
||||
|
||||
**Version 1.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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: <https://github.com/naudio/NAudio>
|
||||
+18
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user