Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4bcbc93e2 | |||
| ca801a006a | |||
| cc1c05add0 | |||
| 969d5e6aa6 | |||
| aaeca76bfd | |||
| 4f6c916bd9 | |||
| ce7dda9e48 | |||
| 80699b27e4 | |||
| 3296a12516 | |||
| 81123ccddf | |||
| 636a62814f | |||
| b5aebaad35 | |||
| bd75f2453c | |||
| c909d1646b | |||
| 5781be2e41 | |||
| 65fea0e5f5 | |||
| 3de6e4a3cb | |||
| e0289962b1 | |||
| 95375c8516 | |||
| 36ea8ddcfc | |||
| 246f0e2511 | |||
| 2e81c42e3b |
@@ -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;
|
namespace HellionChat.Branding;
|
||||||
|
|
||||||
// Lazy-loaded provenance art that ships embedded with the DLL. Two
|
// Lazy-loaded ASCII art that ships embedded with the DLL.
|
||||||
// variants:
|
|
||||||
//
|
//
|
||||||
// - 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
|
// - FoxMini: the four-line fox-head + curly-tail that gets stitched
|
||||||
// into the DI-logger bootstrap line so an xllog reader sees the
|
// into the DI-logger bootstrap line so an xllog reader sees the
|
||||||
// same signature on every plugin load.
|
// same signature on every plugin load.
|
||||||
//
|
//
|
||||||
// Both files live as embedded resources under HellionChat.Branding.* so
|
// The file lives as an embedded resource under HellionChat.Branding.* so
|
||||||
// the plugin DLL is self-contained — no on-disk asset lookup that could
|
// the plugin DLL is self-contained; no on-disk asset lookup that could
|
||||||
// silently miss after a partial deploy.
|
// silently miss after a partial deploy.
|
||||||
internal static class HellionForgeAscii
|
internal static class HellionForgeAscii
|
||||||
{
|
{
|
||||||
private static string? _foxBanner;
|
|
||||||
private static string? _foxMini;
|
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");
|
public static string FoxMini => _foxMini ??= Load("HellionChat.Branding.fox-mini.txt");
|
||||||
|
|
||||||
private static string Load(string resourceName)
|
private static string Load(string resourceName)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 17;
|
private const int LatestVersion = 18;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
@@ -187,6 +187,9 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool CollapseKeepUniqueLinks;
|
public bool CollapseKeepUniqueLinks;
|
||||||
public bool SymbolPickerEnabled = true;
|
public bool SymbolPickerEnabled = true;
|
||||||
public bool PlaySounds = 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 bool KeepInputFocus = true;
|
||||||
public int MaxLinesToRender = 2_500; // 1-10000
|
public int MaxLinesToRender = 2_500; // 1-10000
|
||||||
public bool Use24HourClock = true;
|
public bool Use24HourClock = true;
|
||||||
@@ -282,6 +285,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
||||||
SymbolPickerEnabled = other.SymbolPickerEnabled;
|
SymbolPickerEnabled = other.SymbolPickerEnabled;
|
||||||
PlaySounds = other.PlaySounds;
|
PlaySounds = other.PlaySounds;
|
||||||
|
NotifyFailedTell = other.NotifyFailedTell;
|
||||||
KeepInputFocus = other.KeepInputFocus;
|
KeepInputFocus = other.KeepInputFocus;
|
||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
Use24HourClock = other.Use24HourClock;
|
Use24HourClock = other.Use24HourClock;
|
||||||
@@ -443,6 +447,10 @@ public class Tab
|
|||||||
public bool AllSenderMessages;
|
public bool AllSenderMessages;
|
||||||
public TellTarget TellTarget = TellTarget.Empty();
|
public TellTarget TellTarget = TellTarget.Empty();
|
||||||
|
|
||||||
|
// Per-tab notification sound for messages arriving in an inactive tab.
|
||||||
|
public bool EnableNotificationSound;
|
||||||
|
public uint NotificationSoundId = 1;
|
||||||
|
|
||||||
[NonSerialized]
|
[NonSerialized]
|
||||||
public uint Unread;
|
public uint Unread;
|
||||||
|
|
||||||
@@ -561,6 +569,8 @@ public class Tab
|
|||||||
IsPinned = IsPinned,
|
IsPinned = IsPinned,
|
||||||
AllSenderMessages = AllSenderMessages,
|
AllSenderMessages = AllSenderMessages,
|
||||||
TellTarget = TellTarget.Clone(),
|
TellTarget = TellTarget.Clone(),
|
||||||
|
EnableNotificationSound = EnableNotificationSound,
|
||||||
|
NotificationSoundId = NotificationSoundId,
|
||||||
IsGreeted = IsGreeted,
|
IsGreeted = IsGreeted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
<Version>1.5.4</Version>
|
<Version>1.5.5</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Use lock file to pin exact versions -->
|
<!-- Use lock file to pin exact versions -->
|
||||||
@@ -26,6 +26,11 @@
|
|||||||
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
||||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||||
|
<!-- 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="Pidgin" Version="[3.5.1, 4.0.0)" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4.0.0)" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4.0.0)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -58,8 +63,18 @@
|
|||||||
<EmbeddedResource Include="Resources\Inter-OFL.txt">
|
<EmbeddedResource Include="Resources\Inter-OFL.txt">
|
||||||
<LogicalName>Inter-OFL.txt</LogicalName>
|
<LogicalName>Inter-OFL.txt</LogicalName>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="Resources\Branding\fox-banner.txt">
|
<EmbeddedResource Include="Resources\Branding\fox-banner.png">
|
||||||
<LogicalName>HellionChat.Branding.fox-banner.txt</LogicalName>
|
<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>
|
||||||
<EmbeddedResource Include="Resources\Branding\fox-mini.txt">
|
<EmbeddedResource Include="Resources\Branding\fox-mini.txt">
|
||||||
<LogicalName>HellionChat.Branding.fox-mini.txt</LogicalName>
|
<LogicalName>HellionChat.Branding.fox-mini.txt</LogicalName>
|
||||||
|
|||||||
@@ -35,6 +35,37 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
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)**
|
**v1.5.4 — Polish and Motion (2026-05-20)**
|
||||||
|
|
||||||
A polish cycle: smoother theme switching, faster theme and tab
|
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
|
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
using HellionChat.Integrations;
|
||||||
using HellionChat.Ipc;
|
using HellionChat.Ipc;
|
||||||
using HellionChat.Themes;
|
using HellionChat.Themes;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -85,3 +86,18 @@ internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService s
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
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.Hooking;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using HellionChat._Helpers;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
@@ -330,6 +332,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
Store.UpsertMessage(message);
|
Store.UpsertMessage(message);
|
||||||
|
|
||||||
var currentMatches = Plugin.CurrentTab.Matches(message);
|
var currentMatches = Plugin.CurrentTab.Matches(message);
|
||||||
|
uint? notificationSound = null;
|
||||||
foreach (var tab in Plugin.Config.Tabs)
|
foreach (var tab in Plugin.Config.Tabs)
|
||||||
{
|
{
|
||||||
var unread = !(
|
var unread = !(
|
||||||
@@ -337,7 +340,49 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (tab.Matches(message))
|
if (tab.Matches(message))
|
||||||
|
{
|
||||||
tab.AddMessage(message, unread);
|
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);
|
MessageProcessed?.Invoke(message);
|
||||||
|
|||||||
+10
-5
@@ -115,6 +115,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
||||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||||
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
||||||
|
internal Integrations.CustomAudioPlayer CustomAudioPlayer { get; private set; } = null!;
|
||||||
|
|
||||||
// Platform indirection over Dalamud.Utility.Util. Wired in Phase-1 ctor so
|
// Platform indirection over Dalamud.Utility.Util. Wired in Phase-1 ctor so
|
||||||
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
||||||
@@ -198,10 +199,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
// point (MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize)
|
// point (MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize)
|
||||||
// do not touch either static, so the brief null-window is safe.
|
// do not touch either static, so the brief null-window is safe.
|
||||||
|
|
||||||
// Schema gate: v1.4.x requires config v16+. Users on older schemas
|
// Schema gate: v1.4.x+ requires config v16+. Users on older schemas
|
||||||
// must install v1.4.2 first to run the migration chain. v17 adds
|
// must install v1.4.2 first to run the migration chain. v18 adds the
|
||||||
// Tab.IsPinned (additive, no data migration needed) so v16 configs
|
// per-tab EnableNotificationSound + NotificationSoundId fields and the
|
||||||
// load cleanly and get their Version stamp bumped after the gate.
|
// 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)
|
if (Config.Version < 16)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
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."
|
+ "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
|
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||||
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
||||||
@@ -284,6 +287,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
TypingIpc = _host.Services.GetRequiredService<TypingIpc>();
|
TypingIpc = _host.Services.GetRequiredService<TypingIpc>();
|
||||||
ExtraChat = _host.Services.GetRequiredService<ExtraChat>();
|
ExtraChat = _host.Services.GetRequiredService<ExtraChat>();
|
||||||
HonorificService = _host.Services.GetRequiredService<Integrations.HonorificService>();
|
HonorificService = _host.Services.GetRequiredService<Integrations.HonorificService>();
|
||||||
|
CustomAudioPlayer = _host.Services.GetRequiredService<Integrations.CustomAudioPlayer>();
|
||||||
StatusBar = _host.Services.GetRequiredService<Ui.StatusBar>();
|
StatusBar = _host.Services.GetRequiredService<Ui.StatusBar>();
|
||||||
MessageManager = _host.Services.GetRequiredService<MessageManager>();
|
MessageManager = _host.Services.GetRequiredService<MessageManager>();
|
||||||
AutoTellTabsService = _host.Services.GetRequiredService<AutoTellTabsService>();
|
AutoTellTabsService = _host.Services.GetRequiredService<AutoTellTabsService>();
|
||||||
@@ -335,6 +339,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
new SelfTests.FontPushSmokeStep(this),
|
new SelfTests.FontPushSmokeStep(this),
|
||||||
new SelfTests.WizardStateSmokeStep(this),
|
new SelfTests.WizardStateSmokeStep(this),
|
||||||
new SelfTests.QuickPickerSelfTestStep(this),
|
new SelfTests.QuickPickerSelfTestStep(this),
|
||||||
|
new SelfTests.FoxBannerTextureSmokeStep(this),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Re-surface the wizard for existing users when a major UX
|
// 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<ILogger<Integrations.HonorificService>>(),
|
||||||
sp.GetRequiredService<IFramework>()
|
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(
|
services.AddSingleton(sp => new MessageManager(
|
||||||
sp.GetRequiredService<Plugin>(),
|
sp.GetRequiredService<Plugin>(),
|
||||||
@@ -172,6 +178,11 @@ internal static class PluginHostFactory
|
|||||||
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||||
sp.GetRequiredService<AutoTellTabsService>()
|
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_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_Name => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Name));
|
||||||
internal static string Settings_ThemeAndLayout_ReduceMotion_Description => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Description));
|
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">
|
<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>
|
<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>
|
</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>
|
</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.Interface.Windowing;
|
||||||
using Dalamud.Memory;
|
using Dalamud.Memory;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions;
|
using HellionChat.GameFunctions;
|
||||||
using HellionChat.GameFunctions.Types;
|
using HellionChat.GameFunctions.Types;
|
||||||
@@ -776,6 +777,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
// (~17ms at 60fps) late, invisible inside the post-reload Atlas-Build.
|
// (~17ms at 60fps) late, invisible inside the post-reload Atlas-Build.
|
||||||
private bool _firstFrameDone;
|
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()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
DrewThisFrame = true;
|
DrewThisFrame = true;
|
||||||
@@ -1114,6 +1123,38 @@ public sealed class ChatLogWindow : Window
|
|||||||
using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, normalColor);
|
using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, normalColor);
|
||||||
if (ImGui.Selectable(Language.ChatLog_HideChat))
|
if (ImGui.Selectable(Language.ChatLog_HideChat))
|
||||||
UserHide();
|
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,
|
Tab tab,
|
||||||
PayloadHandler handler,
|
PayloadHandler handler,
|
||||||
float childHeight,
|
float childHeight,
|
||||||
bool switchedTab
|
bool switchedTab,
|
||||||
|
bool updateScrollState = true
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
using var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight));
|
using (var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight)))
|
||||||
if (!child.Success)
|
{
|
||||||
return;
|
if (child.Success)
|
||||||
|
{
|
||||||
|
if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps)
|
||||||
|
DrawLogTableStyle(tab, handler, switchedTab);
|
||||||
|
else
|
||||||
|
DrawLogNormalStyle(tab, handler, switchedTab);
|
||||||
|
|
||||||
if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps)
|
// Cached for the header toolbar's scroll-to-bottom button, which is
|
||||||
DrawLogTableStyle(tab, handler, switchedTab);
|
// drawn one frame later. GetScrollMaxY / GetScrollY here refer to
|
||||||
else
|
// the child's scroll context. Pop-out windows pass updateScrollState:
|
||||||
DrawLogNormalStyle(tab, handler, switchedTab);
|
// 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)
|
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))
|
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
|
||||||
DrawMessages(tab, handler, false);
|
DrawMessages(tab, handler, false);
|
||||||
|
|
||||||
if (switchedTab || ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
|
if (switchedTab || _scrollToBottomRequested || ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
|
||||||
ImGui.SetScrollHereY(1f);
|
ImGui.SetScrollHereY(1f);
|
||||||
|
_scrollToBottomRequested = false;
|
||||||
|
|
||||||
handler.Draw();
|
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
|
// Custom styles can have cellPadding that go above 4, which GetScrollY isn't respecting
|
||||||
var cellPaddingOffset =
|
var cellPaddingOffset =
|
||||||
!compact && oldCellPadding.Y > 4f ? oldCellPadding.Y - 4f : 0f;
|
!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);
|
ImGui.SetScrollHereY(1f);
|
||||||
|
_scrollToBottomRequested = false;
|
||||||
|
|
||||||
handler.Draw();
|
handler.Draw();
|
||||||
}
|
}
|
||||||
@@ -2299,14 +2361,50 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.WantedTab = null;
|
Plugin.WantedTab = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DrawChatHeaderToolbar: renders the pop-out button for the active tab.
|
// DrawChatHeaderToolbar: renders the honorific title slot, the optional
|
||||||
// v1.3.0 also renders the optional Honorific title slot left of it.
|
// scroll-to-bottom button, and the pop-out button for the active tab.
|
||||||
private void DrawChatHeaderToolbar(Tab tab)
|
private void DrawChatHeaderToolbar(Tab tab)
|
||||||
{
|
{
|
||||||
DrawHonorificTitleSlot();
|
DrawHonorificTitleSlot();
|
||||||
|
DrawScrollToBottomToolbarButton();
|
||||||
DrawPopOutButton(tab);
|
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)
|
private void DrawPopOutButton(Tab tab)
|
||||||
{
|
{
|
||||||
var avail = ImGui.GetContentRegionAvail().X;
|
var avail = ImGui.GetContentRegionAvail().X;
|
||||||
@@ -2358,7 +2456,13 @@ public sealed class ChatLogWindow : Window
|
|||||||
crownWidth = ImGui.CalcTextSize(FontAwesomeIcon.Crown.ToIconString()).X;
|
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)
|
if (maxTitleWidth <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -2484,8 +2588,13 @@ public sealed class ChatLogWindow : Window
|
|||||||
var anyChanged = false;
|
var anyChanged = false;
|
||||||
var tabs = Plugin.Config.Tabs;
|
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);
|
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;
|
anyChanged = true;
|
||||||
|
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.TrashAlt, tooltip: Language.ChatLog_Tabs_Delete))
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.TrashAlt, tooltip: Language.ChatLog_Tabs_Delete))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using HellionChat.Branding;
|
using HellionChat.Branding;
|
||||||
@@ -171,24 +172,41 @@ public sealed class FirstRunWizard : Window
|
|||||||
ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title);
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title);
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
// Banner is opt-in: the full silhouette dominates the wizard window
|
// Fox-banner image: the embedded Hellion Forge fox artwork. The card
|
||||||
// at the default size, so the TreeNode is folded by default and the
|
// behind the image gives the dark fox enough contrast against the
|
||||||
// onboarding copy stays the primary focus. Mirrors the pre-rewrite
|
// plugin's dark UI so the logo reads clearly at a glance.
|
||||||
// collapsible anchor from v1.5.1.
|
var banner = FoxBannerTexture.Shared.GetWrapOrDefault();
|
||||||
using (var tree = ImRaii.TreeNode("Hellion Forge"))
|
if (banner is not null)
|
||||||
{
|
{
|
||||||
if (tree.Success)
|
const uint CardColor = 0xFFE8E8E8; // off-white fill so the dark fox pops
|
||||||
{
|
var imgHeight = 170f * ImGuiHelpers.GlobalScale;
|
||||||
using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
|
var imgWidth = imgHeight * banner.Size.X / banner.Size.Y;
|
||||||
{
|
var pad = 14f * ImGuiHelpers.GlobalScale;
|
||||||
// CalcTextSize must run inside the MonoFont push so the
|
var cardWidth = imgWidth + pad * 2f;
|
||||||
// measurement matches the glyph width actually used for
|
var cardHeight = imgHeight + pad * 2f;
|
||||||
// rendering.
|
var rounding = 8f * ImGuiHelpers.GlobalScale;
|
||||||
var bannerSize = ImGui.CalcTextSize(HellionForgeAscii.FoxBanner);
|
|
||||||
ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - bannerSize.X) * 0.5f);
|
// Centre the card in the content region. Clamp to zero so the card
|
||||||
ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
|
// 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();
|
ImGui.Spacing();
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ internal class Popout : Window
|
|||||||
|
|
||||||
var handler = ChatLogWindow.HandlerLender.Borrow();
|
var handler = ChatLogWindow.HandlerLender.Borrow();
|
||||||
var logHeight = ImGui.GetContentRegionAvail().Y - inputBarHeight - hintBannerHeight;
|
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)
|
if (inputEnabled && InputBar != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -145,6 +145,12 @@ internal sealed class Chat : ISettingsTab
|
|||||||
ref Mutable.SymbolPickerEnabled
|
ref Mutable.SymbolPickerEnabled
|
||||||
);
|
);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_SymbolPicker_Enable_Description);
|
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.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
@@ -68,18 +69,38 @@ internal sealed class Information : ISettingsTab
|
|||||||
DrawChangelogSection();
|
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()
|
private void DrawHellionForgeSection()
|
||||||
{
|
{
|
||||||
using var tree = ImRaii.TreeNode("Hellion Forge");
|
var banner = FoxBannerTexture.Shared.GetWrapOrDefault();
|
||||||
if (!tree.Success)
|
if (banner is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
const uint CardColor = 0xFFE8E8E8; // off-white fill so the dark fox pops
|
||||||
using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
|
var imgHeight = 170f * ImGuiHelpers.GlobalScale;
|
||||||
ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
|
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()
|
private void DrawVersionInfoSection()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui;
|
|||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
@@ -165,6 +166,78 @@ internal sealed class Tabs : ISettingsTab
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp);
|
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);
|
ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut);
|
||||||
if (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",
|
"resolved": "4.4.0",
|
||||||
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
|
"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": {
|
"Pidgin": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.5.1, 4.0.0)",
|
"requested": "[3.5.1, 4.0.0)",
|
||||||
@@ -366,6 +375,11 @@
|
|||||||
"resolved": "17.11.4",
|
"resolved": "17.11.4",
|
||||||
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
||||||
},
|
},
|
||||||
|
"NAudio.Core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.2.1",
|
||||||
|
"contentHash": "GgkdP6K/7FqXFo7uHvoqGZTJvW4z8g2IffhOO4JHaLzKCdDOUEzVKtveoZkCuUX8eV2HAINqi7VFqlFndrnz/g=="
|
||||||
|
},
|
||||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.11",
|
"resolved": "2.1.11",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||||
[](https://github.com/goatcorp/Dalamud)
|
[](https://github.com/goatcorp/Dalamud)
|
||||||
[](https://dotnet.microsoft.com/)
|
[](https://dotnet.microsoft.com/)
|
||||||
[](https://www.finalfantasyxiv.com/)
|
[](https://www.finalfantasyxiv.com/)
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Version 1.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).
|
[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
|
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
|
## Project Status
|
||||||
|
|
||||||
**Version 1.5.4** — Polish and Motion. Theme switches now crossfade smoothly over
|
**Version 1.5.5** — Upstream-Sync Tab-Features. Failed tells now raise a warning toast
|
||||||
~300 ms across every Hellion-rendered surface — sidebar, title, buttons, tabs,
|
when a message could not be delivered (recipient offline, in an instance, or blocking
|
||||||
scrollbar, separators. The window background snaps deliberately so the per-window
|
you). Per-tab notification sounds let each tab play one of the 16 game chat sounds or
|
||||||
opacity override from Dalamud's pinning menu stays intact. A new header quick-picker —
|
three bundled Hellion sounds when a message arrives on a background tab, with a
|
||||||
a palette button left of the cog — opens a compact popup that switches themes and tabs
|
preview button. The tab rename field in the right-click menu auto-focuses on open and
|
||||||
without opening Settings; the active entry carries a check glyph and the popup stays
|
accepts up to 512 characters. A jump-to-latest button appears in the chat log header
|
||||||
open between picks. Sidebar icons ease their opacity on hover and card-mode message
|
while scrolled up from the live end. Map-flag and item-link insertion is available from
|
||||||
borders highlight per tab, both framerate-independent so a stalled Wine frame cannot
|
the chat input right-click menu. The Hellion Forge fox banner in the first-run wizard
|
||||||
overshoot. A new "Reduce motion" toggle in Theme & Layout disables the crossfade, the
|
and the Information tab is now a real image. Schema bumped to v18, additive fields
|
||||||
hover animations and the unread-tab pulse for users who prefer a static UI. No schema
|
only, no data migration.
|
||||||
bump, migration v17 stays.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
## 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
|
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
|
## Next Cycle
|
||||||
|
|
||||||
**Plugin Integrations Wave 2-6** (Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM
|
**v1.5.5b — Upstream-Sync Filter/Notification/Polish** is the next planned scope: the second half
|
||||||
Quick-DM) is the next planned scope. The UiBuilder first-frame HITCH investigation that v1.5.1
|
of the backlog-sync wave, covering filter improvements, notification refinements, and UI polish
|
||||||
queued is now closed as a side effect of v1.5.3's font-stack fix — HITCH dropped from ~74 ms into
|
items that did not fit the v1.5.5 bundle. Plugin Integrations Wave 2-6 (Context-Menu,
|
||||||
the 15-25 ms range. The Wine/Linux scroll-rubber-band spike remains at the tail.
|
NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM) follows.
|
||||||
|
|
||||||
Native-speaker review of the AI-assisted v1.5.3 translations (13 legacy Crowdin locales) runs in
|
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.
|
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)
|
## 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
|
A polish cycle of three P3 items. Theme switches crossfade over ~300 ms across every
|
||||||
|
|||||||
Reference in New Issue
Block a user