feat(ui): notify on failed tell via RaptureLogModule hook
This commit is contained in:
@@ -187,6 +187,8 @@ public class Configuration : IPluginConfiguration
|
||||
public bool CollapseKeepUniqueLinks;
|
||||
public bool SymbolPickerEnabled = true;
|
||||
public bool PlaySounds = true;
|
||||
// UI-2: 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 +284,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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
// UI-2: 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 — that stays v1.5.6.
|
||||
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
|
||||
{
|
||||
// DISCOVERY (Task 2 Step 9): while the id set is empty, log every
|
||||
// call so a failed tell can be triggered in-game and its id read.
|
||||
// Remove this block once FailedTellLogMessageIds is pinned.
|
||||
_logger.LogInformation(
|
||||
"ShowLogMessageString id={Id} value={Value}",
|
||||
logMessageId,
|
||||
value is null ? "<null>" : value->ToString());
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,8 @@ 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 MessageManager(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
@@ -172,6 +174,8 @@ internal static class PluginHostFactory
|
||||
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||
sp.GetRequiredService<AutoTellTabsService>()
|
||||
));
|
||||
services.AddHostedService(sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
|
||||
sp.GetRequiredService<Integrations.FailedTellNotifier>()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -451,4 +451,20 @@ 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));
|
||||
|
||||
// Hellion Chat — v1.5.5 UI-2 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));
|
||||
|
||||
// Hellion Chat — v1.5.5 UI-3 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));
|
||||
|
||||
// Hellion Chat — v1.5.5 UI-5a Scroll-to-bottom + UI-6 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,40 @@
|
||||
<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>
|
||||
|
||||
<!-- Hellion Chat — v1.5.5 UI-2 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>
|
||||
|
||||
<!-- Hellion Chat — v1.5.5 UI-3 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>
|
||||
|
||||
<!-- Hellion Chat — v1.5.5 UI-5a Scroll-to-bottom + UI-6 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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace HellionChat._Helpers;
|
||||
|
||||
// UI-2 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 discovery (see plan Task 2, Step 9). An empty set means the
|
||||
// hook never toasts — "no toast over a false toast" until ids are pinned.
|
||||
public static readonly IReadOnlySet<uint> FailedTellLogMessageIds = new HashSet<uint>
|
||||
{
|
||||
// pinned in Task 2 Step 9
|
||||
};
|
||||
|
||||
public static bool ShouldNotify(uint logMessageId, bool notifyEnabled, IReadOnlySet<uint> failedTellIds)
|
||||
=> notifyEnabled && failedTellIds.Contains(logMessageId);
|
||||
}
|
||||
Reference in New Issue
Block a user