7a1bd1babc
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.
199 lines
6.9 KiB
C#
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;
|
|
}
|
|
}
|