Files
HellionChat/HellionChat/Services/TellRouterService.cs
T
JonKazama-Hellion 5b738e6885 chore: comments say what the code does, not which task produced it
A comment that reads "MUST stay in lockstep with TryGetActiveCrossfade (K8)"
helps nobody outside the plan that used to have a K8 in it, and the plans are
not in this repo. Same for "Spec FR-4", "plan §B.2", "Sub-Task 4.4" and the
F/R/M/A/S round codes scattered through the style engine and the self-tests.

Personal names go too. "tester feedback from Jin (v1.4.7)" and "Flo decision
2026-06-15" carry the reason fine without naming anyone -- the version and the
reason are the parts a reader can act on, and a public repo should not need a
cast list to be read.

The rule applied throughout: keep the why, drop the reference. Version numbers
stay, since those resolve through the changelog. 77 files.

ChunkUtil also carried 281 lines of commented-out code -- an older ToChunks
variant and two helpers with no callers, inherited and never removed. Deleted;
git remembers them.
2026-08-19 21:50:31 +02:00

115 lines
4.6 KiB
C#

using HellionChat.Code;
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Services;
// Routes an incoming tell to the configured TellAutoOpenMode (Off/Sidebar/
// TopTab/Popout). Deliberately decoupled from AutoTellTabsService:
// that service owns tab CREATION + lifecycle; this only REVEALS/pops the tab it
// finds. Popout guards on pool.IsOpen so it never double-pops a tab the
// AutoTellTabsOpenAsPopout path already opened. Subscribes to the resolved
// MessageManager.MessageProcessed stream (a resolved Message), not the raw
// IChatGui event, and defers the reveal one tick so the tab exists regardless of
// subscriber order. Wired by TellRouterServiceInitHostedService.
internal sealed class TellRouterService : IDisposable
{
private readonly MessageManager _messageManager;
private readonly ILogger<TellRouterService> _logger;
private bool _initialized;
public TellRouterService(MessageManager messageManager, ILogger<TellRouterService> logger)
{
_messageManager = messageManager;
_logger = logger;
}
public void Initialize()
{
if (_initialized)
return;
_messageManager.MessageProcessed += OnMessageProcessed;
_initialized = true;
_logger.LogDebug("TellRouterService online; routing incoming tells by TellAutoOpenMode.");
}
public void Dispose()
{
if (!_initialized)
return;
_messageManager.MessageProcessed -= OnMessageProcessed;
_initialized = false;
}
private void OnMessageProcessed(Message message)
{
var mode = Plugin.Config.TellAutoOpenMode;
if (mode == TellAutoOpenMode.Off)
return;
if (message.Code.Type != ChatType.TellIncoming)
return;
// Partner = sender for an incoming tell. Same payload idiom AutoTellTabs uses
// (AutoTellTabsService.ExtractTellPartner), so the lookup never diverges.
var partner =
ChunkUtil.TryGetPlayerPayload(message.Sender)
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
if (partner == null)
return;
var name = partner.PlayerName;
var world = partner.World.RowId;
// Defer the reveal to the next framework tick. AutoTellTabsService also
// handles this MessageProcessed (synchronously); by the next tick the tab
// exists regardless of subscription order, and the reveal (ActivateTab / pool
// mutation) is serialized with Draw (reference_dalamud_framework_thread).
Plugin.Framework.RunOnFrameworkThread(() =>
{
// Lock-safe lookup: AutoTellTabs mutates Config.Tabs under its lock on the
// worker thread, so we read through its guarded accessor, not the static.
var tab = Plugin.Instance.AutoTellTabsService?.FindTempTabSafe(name, world);
if (tab == null)
return; // nothing to reveal (auto-tell-tabs off -> no tab created)
// Switching to the tab on every tell is user-gated
// (TellAutoOpenSwitchAlways, default on); when off the tab still
// appears with its unread badge but the active tab is left alone. A
// tab that is already popped out needs no reveal at all -- it is on
// screen, and pulling the main window onto it costs the user the tab
// they were reading.
var reveal = TabLifecycleHelpers.PlanTellReveal(
mode,
Plugin.Config.TellAutoOpenSwitchAlways,
Plugin.Instance.ChannelPopoutPool.IsOpen(tab.Identifier)
);
switch (reveal)
{
case TabLifecycleHelpers.TellReveal.MainWindow:
// The mode also picks the layout, so Sidebar vs TopTab are
// actually distinct outcomes, not the same ActivateTab.
var wantLayout =
mode == TellAutoOpenMode.TopTab
? MainWindowLayoutMode.TopTabs
: MainWindowLayoutMode.Sidebar;
if (Plugin.Config.MainWindowLayoutMode != wantLayout)
{
Plugin.Config.MainWindowLayoutMode = wantLayout;
Plugin.Instance.SaveConfig();
}
Plugin.Instance.MainWindow?.ActivateTab(tab);
break;
case TabLifecycleHelpers.TellReveal.Popout:
Plugin.Instance.ChannelPopoutPool.TryOpen(tab);
break;
}
});
}
}