Files
HellionChat/HellionChat/Infrastructure/Hosting/InitHostedServices.cs
T
JonKazama-Hellion b9feb8650f chore(comments): drop the spec task codes the last pass missed
Codes like POP-1c or B4b-2 name a task in a planning document, not
anything in the code. A reader has no way to resolve them and they age
into noise the moment the document is closed. Where a code was used as a
reference, the sentence now names the function it meant.
2026-08-20 07:54:45 +02:00

209 lines
8.0 KiB
C#

using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Plugin;
using HellionChat.Integrations;
using HellionChat.Ipc;
using HellionChat.Themes;
using HellionChat.Ui;
using HellionChat.Ui.Components;
using HellionChat.Ui.Windows;
using Microsoft.Extensions.Hosting;
namespace HellionChat.Infrastructure.Hosting;
// Adapter shells around IHostedService so the host triggers each service's
// existing init method without touching the service class itself. Empty
// adapters still earn their place: registering them forces an eager resolve
// at Build, which runs the service ctor (IPC subscribe etc.) right then
// instead of lazily on first GetRequiredService.
internal sealed class ThemeRegistryInitHostedService(
ThemeRegistry registry,
FontManager fontManager
) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
// Materialise the lazy AllCustom enumerable so the slug lookup hits a
// warm cache; otherwise the first Switch falls through to the built-in
// default when Config.Theme points at a custom slug.
foreach (var _ in registry.AllCustom()) { }
registry.SwitchSilent(Plugin.Config.Theme);
// Point font sizes at the active theme's typography, wire future
// theme switches to the atlas rebuild, and apply the boot theme's override.
fontManager.SetTypographySource(() => registry.Active.Typography);
registry.SetActiveChangedCallback(() => fontManager.RebuildDelegateFontsIfChanged());
await Plugin.Framework.RunOnFrameworkThread(() =>
fontManager.RebuildDelegateFontsIfChanged()
);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
// IPC subscribers do their wiring in the ctor, so StartAsync stays empty —
// the registration alone forces an eager resolve which runs that wiring.
internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService
{
private readonly IpcManager _ipc = ipc;
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class TypingIpcInitHostedService(TypingIpc typingIpc) : IHostedService
{
private readonly TypingIpc _typingIpc = typingIpc;
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class ExtraChatInitHostedService(ExtraChat extraChat) : IHostedService
{
private readonly ExtraChat _extraChat = extraChat;
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class MessageManagerInitHostedService(
IDalamudPluginInterface pluginInterface,
MessageManager manager
) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
// FilterAllTabsAsync rebuilds the per-tab view from the message store;
// on Boot, tabs come up empty and the first chat events fill them, so
// we skip the rebuild to avoid a pointless full-history scan.
if (pluginInterface.Reason is not PluginLoadReason.Boot)
manager.FilterAllTabsAsync();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService service)
: IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
service.Initialize();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class TellRouterServiceInitHostedService(Services.TellRouterService service)
: IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
service.Initialize();
return 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;
}
internal sealed class PayloadHandlerInitHostedService(
PayloadHandler payloadHandler,
MessageList messageList
) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
// 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(() =>
{
// Single call using the params-overload removes the delegate from all addons it was registered for (ItemDetail + ActionDetail both cleaned in one shot).
Plugin.AddonLifecycle.UnregisterListener(payloadHandler.MoveTooltip);
});
}
}
// Wires MainWindow into CommandHelpWindow post-container-build. CommandHelpWindow
// cannot take MainWindow as a ctor-param because that would close the cycle
// InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch
// it through FactoryCallSite registrations and the resolve recurses silently).
// Both singletons exist by host.StartAsync time, so this is the first safe point
// to wire the setter — same setter-injection pattern as MessageList.AttachPayloadHandler.
internal sealed class CommandHelpWindowInitHostedService(
CommandHelpWindow commandHelpWindow,
MainWindow mainWindow
) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
commandHelpWindow.AttachMainWindow(mainWindow);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
// Attaches the singleton PayloadHandler to every pre-allocated pop-out
// window's MessageList post-container-build. Pool/window cannot take the
// PayloadHandler via ctor (that would close the silent FactoryCallSite cycle —
// same setter-injection reason as MessageList.AttachPayloadHandler / CommandHelpWindow.
// AttachMainWindow). Both singletons exist by host.StartAsync time.
internal sealed class ChannelPopoutInitHostedService(
ChannelPopoutPool pool,
PayloadHandler payloadHandler
) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
foreach (var window in pool.Instances)
window.AttachPayloadHandler(payloadHandler);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}