4000bbd199
Security / scan (push) Successful in 12s
Updated .editorconfig to set indent_style=space and indent_size=4 for C# files. Reformat all .cs files to apply the new indentation settings. No code logic changes, just whitespace reformatting. also updated some comments in files in shorter and Precise way. No logic changes, just comment rewording for clarity and conciseness.
197 lines
6.8 KiB
C#
197 lines
6.8 KiB
C#
using System;
|
|
using Dalamud.Plugin;
|
|
using Dalamud.Plugin.Ipc;
|
|
using Dalamud.Plugin.Services;
|
|
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 IPluginLog _log;
|
|
private readonly IFramework _framework;
|
|
private bool _versionWarningLogged;
|
|
|
|
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,
|
|
IPluginLog log,
|
|
IFramework framework
|
|
)
|
|
{
|
|
_framework = framework;
|
|
_log = log;
|
|
|
|
// 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));
|
|
}
|
|
|
|
private void TryInitialPull()
|
|
{
|
|
try
|
|
{
|
|
var version = _apiVersion.InvokeFunc();
|
|
DetectedApiVersion = version;
|
|
|
|
if (!IsApiVersionCompatible(version))
|
|
{
|
|
if (!_versionWarningLogged)
|
|
{
|
|
_log.Warning(
|
|
"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.
|
|
_log.Debug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
|
|
IsAvailable = false;
|
|
CurrentTitle = null;
|
|
}
|
|
}
|
|
|
|
private void OnTitleChanged(string json)
|
|
{
|
|
// Skip updates on version mismatch; subscription stays live for reload.
|
|
if (!IsAvailable)
|
|
return;
|
|
CurrentTitle = ParseTitleJson(json);
|
|
}
|
|
|
|
private void OnReady()
|
|
{
|
|
// Schedule on framework thread — NotifyReady can dispatch from any thread.
|
|
_framework.RunOnFrameworkThread(TryInitialPull);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private void TryUnsubscribe(Action unsubscribe)
|
|
{
|
|
try
|
|
{
|
|
unsubscribe();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Debug(ex, "Honorific unsubscribe failed (likely already gone).");
|
|
}
|
|
}
|
|
|
|
// Threading: IPC events and ImGui both run on the framework thread, so
|
|
// OnTitleChanged and the render path never race — no volatile/Interlocked
|
|
// needed as long as Dalamud's framework-thread delivery contract holds.
|
|
//
|
|
// Constructor and OnReady are exceptions: they run outside that contract
|
|
// (plugin-loader thread and Honorific's NotifyReady respectively), so both
|
|
// use RunOnFrameworkThread to safely reach ObjectTable.LocalPlayer.
|
|
|
|
// --- Pure-logic helpers; tested via HellionChat.Tests/Integrations. ---
|
|
|
|
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;
|
|
}
|
|
}
|