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.
115 lines
4.6 KiB
C#
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;
|
|
}
|
|
});
|
|
}
|
|
}
|