Files
HellionChat/HellionChat/Integrations/HonorificService.cs
T
JonKazama-Hellion 7a1bd1babc refactor(di): migrate Integrations + IPC layer to ILogger<T> (DI-4 Slice B)
Seven services across Integrations/, Ipc/ and GameFunctions/ shift
from Plugin.LogProxy to Microsoft.Extensions.Logging.ILogger<T>.

Files with live LogProxy sites (10 in total):
- Ipc/ExtraChat (1)
- GameFunctions/Chat (6)
- GameFunctions/GameFunctions (2)
- GameFunctions/KeybindManager (1)

Foundation-touch files (no current sites, ctor takes ILogger<T> as
seed for the v1.5.7-11 Plugin-Integrations wave):
- Integrations/HonorificService (also drops the local IPluginLog
  _log field in favour of ILogger<HonorificService> _logger; the
  three _log.* calls there are migrated as a bonus since the field
  had to change anyway)
- IpcManager
- Ipc/TypingIpc

GameFunctions takes ILoggerFactory as an extra ctor arg so it can
hand a typed logger to its nested Chat and KeybindManager (same
pattern MessageStore + MessageEnumerator use in Slice A).

PluginHostFactory factory lambdas updated for all five Slice B
services that need extra resolves.

Plan drift D8: GameFunctions.TryOpenAdventurerPlate is an internal
static method whose only Warning call cannot reach the instance
_logger. The one site stays on Plugin.LogProxy with an inline note;
promoting it to instance + PayloadHandler.cs:814 call-site update is
a v1.5.1+ cleanup, out of DI-4 Slice B scope.
2026-05-17 09:56:46 +02:00

199 lines
6.9 KiB
C#

using System;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace HellionChat.Integrations;
// Newtonsoft.Json is used here for IPC compatibility with Honorific, which
// serialises TitleData with it. It's a transitive Dalamud dependency — no
// new NuGet entry needed. The rest of HellionChat uses System.Text.Json.
internal sealed class HonorificService : IDisposable
{
private const string IpcNamespace = "Honorific";
// Major version of the Honorific IPC contract we're built against.
internal const uint ExpectedApiMajor = 3;
// IPC gates — kept as fields so Dispose can unsubscribe the same instances.
private readonly ICallGateSubscriber<(uint, uint)> _apiVersion;
private readonly ICallGateSubscriber<string> _getLocalCharacterTitle;
private readonly ICallGateSubscriber<string, object> _localCharacterTitleChanged;
private readonly ICallGateSubscriber<object> _ready;
private readonly ICallGateSubscriber<object> _disposing;
private readonly ILogger<HonorificService> _logger;
private readonly IFramework _framework;
private bool _versionWarningLogged;
// Thread: framework only — IPC delivery + ImGui render both run there.
public HonorificTitleData? CurrentTitle { get; private set; }
public bool IsAvailable { get; private set; }
public (uint Major, uint Minor)? DetectedApiVersion { get; private set; }
public HonorificService(
IDalamudPluginInterface pluginInterface,
ILogger<HonorificService> logger,
IFramework framework
)
{
_framework = framework;
_logger = logger;
// Gate objects are cached per-name by Dalamud and safe to register
// before Honorific loads — they just won't fire until it does.
// Initial pull is scheduled on the framework thread because plugin
// constructors run on the loader thread, and Honorific's IPC handlers
// read ObjectTable.LocalPlayer which throws off the framework thread.
_apiVersion = pluginInterface.GetIpcSubscriber<(uint, uint)>($"{IpcNamespace}.ApiVersion");
_getLocalCharacterTitle = pluginInterface.GetIpcSubscriber<string>(
$"{IpcNamespace}.GetLocalCharacterTitle"
);
_localCharacterTitleChanged = pluginInterface.GetIpcSubscriber<string, object>(
$"{IpcNamespace}.LocalCharacterTitleChanged"
);
_ready = pluginInterface.GetIpcSubscriber<object>($"{IpcNamespace}.Ready");
_disposing = pluginInterface.GetIpcSubscriber<object>($"{IpcNamespace}.Disposing");
_localCharacterTitleChanged.Subscribe(OnTitleChanged);
_ready.Subscribe(OnReady);
_disposing.Subscribe(OnDisposing);
_framework.RunOnFrameworkThread(TryInitialPull);
}
public void Dispose()
{
// Wrap each unsubscribe — a missing gate must not block the others.
// Leaking a subscription keeps this service alive across plugin reloads.
TryUnsubscribe(() => _localCharacterTitleChanged.Unsubscribe(OnTitleChanged));
TryUnsubscribe(() => _ready.Unsubscribe(OnReady));
TryUnsubscribe(() => _disposing.Unsubscribe(OnDisposing));
}
// Thread: framework (scheduled from ctor and OnReady).
private void TryInitialPull()
{
try
{
var version = _apiVersion.InvokeFunc();
DetectedApiVersion = version;
if (!IsApiVersionCompatible(version))
{
if (!_versionWarningLogged)
{
_logger.LogWarning(
"Honorific API version mismatch — expected major 3, "
+ "found {Major}.{Minor}. Disabling Honorific integration.",
version.Item1,
version.Item2
);
_versionWarningLogged = true;
}
IsAvailable = false;
return;
}
IsAvailable = true;
_versionWarningLogged = false;
var json = _getLocalCharacterTitle.InvokeFunc();
CurrentTitle = ParseTitleJson(json);
}
catch (Exception ex)
{
// Honorific not installed or not yet initialised — Ready will retry.
_logger.LogDebug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
IsAvailable = false;
CurrentTitle = null;
}
}
// Thread: framework (Dalamud IPC delivery contract).
private void OnTitleChanged(string json)
{
// Skip updates on version mismatch; subscription stays live for reload.
if (!IsAvailable)
return;
CurrentTitle = ParseTitleJson(json);
}
// Thread: any (Honorific dispatches NotifyReady from its own thread).
private void OnReady()
{
_framework.RunOnFrameworkThread(TryInitialPull);
}
// Thread: framework (IPC delivery contract); idempotent — Disposing fires once.
private void OnDisposing()
{
// Honorific unloading — clear cached state so the header hides next frame.
// Subscriptions stay registered in case Honorific reloads.
// CurrentTitle is already nulled by OnTitleChanged before this fires,
// re-clearing here is belt-and-braces.
CurrentTitle = null;
IsAvailable = false;
DetectedApiVersion = null;
}
// Thread: framework (called from Dispose, which runs on the framework
// cleanup block in Plugin.DisposeAsync).
private void TryUnsubscribe(Action unsubscribe)
{
try
{
unsubscribe();
}
catch (Exception ex)
{
// Warning not Debug — a silent unsubscribe failure leaks a live
// subscription across plugin reloads.
_logger.LogWarning(
ex,
"Honorific unsubscribe failed (likely API break or gate already gone)."
);
}
}
internal static HonorificTitleData? ParseTitleJson(string json)
{
if (string.IsNullOrEmpty(json))
return null;
try
{
return JsonConvert.DeserializeObject<HonorificTitleData>(json);
}
catch (JsonException)
{
return null;
}
}
internal static bool IsApiVersionCompatible((uint Major, uint Minor) apiVersion)
{
return apiVersion.Major == ExpectedApiMajor;
}
internal static bool ShouldRenderSlot(
bool toggleEnabled,
bool isAvailable,
HonorificTitleData? title
)
{
if (!toggleEnabled)
return false;
if (!isAvailable)
return false;
if (title is null)
return false;
if (title.IsOriginal)
return false;
if (string.IsNullOrEmpty(title.Title))
return false;
return true;
}
}