feat(infra): wire PayloadHandlerInitHostedService (AddonLifecycle + setter)

G connects PayloadHandler to the runtime — this is the activation point
after E1-E6 built the type and F registered it in DI:

- StartAsync calls MessageList.AttachPayloadHandler(_payloadHandler) to
  complete the §6.2 cycle-resolution (ctor-cycle was broken by setter,
  this is where the setter actually fires)
- StartAsync registers AddonLifecycle listener for MoveTooltip on
  PostUpdate of "ItemDetail" and "ActionDetail" addons
- StopAsync unregisters the listener
- Both Register/Unregister wrapped in Plugin.Framework.RunOnFrameworkThread
  as defensive insurance — IAddonLifecycle thread-affinity is not
  explicitly documented in Dalamud API; wrap keeps the v1.5.6 runtime
  contract intact (per spec §5-G note)

Mirrors existing IpcManagerInitHostedService / TypingIpcInitHostedService
pattern in Infrastructure/Hosting/. PluginHostFactory adds the
AddHostedService<PayloadHandlerInitHostedService>() registration.

After G, the chunked-message-render pipeline is end-to-end functional:
MessageList renders via ChunkRenderer, _handler is wired so popups fire
on click/hover, MoveTooltip repositions native item-tooltips away from
the chat window.
This commit is contained in:
2026-05-27 20:03:52 +02:00
parent 4fb5ee6128
commit 931a152d00
2 changed files with 44 additions and 0 deletions
@@ -1,7 +1,9 @@
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Plugin;
using HellionChat.Integrations;
using HellionChat.Ipc;
using HellionChat.Themes;
using HellionChat.Ui.Components;
using Microsoft.Extensions.Hosting;
namespace HellionChat.Infrastructure.Hosting;
@@ -101,3 +103,41 @@ internal sealed class FailedTellNotifierInitHostedService(FailedTellNotifier not
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class PayloadHandlerInitHostedService(
PayloadHandler payloadHandler,
MessageList messageList
) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
// §6.2 cycle-resolution: both singletons exist by the time HostedServices
// run, so this is the first safe point to wire the setter.
messageList.AttachPayloadHandler(payloadHandler);
// IAddonLifecycle thread-affinity is not explicitly documented; wrap is
// defensive insurance — mirrors the window-registration RunOnFrameworkThread
// pattern established in PluginLifecycle.cs.
await Plugin.Framework.RunOnFrameworkThread(() =>
{
Plugin.AddonLifecycle.RegisterListener(
AddonEvent.PostUpdate,
"ItemDetail",
payloadHandler.MoveTooltip
);
Plugin.AddonLifecycle.RegisterListener(
AddonEvent.PostUpdate,
"ActionDetail",
payloadHandler.MoveTooltip
);
});
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Plugin.Framework.RunOnFrameworkThread(() =>
{
Plugin.AddonLifecycle.UnregisterListener(payloadHandler.MoveTooltip);
});
}
}