fix(integrations): schedule Honorific initial pull on framework thread

This commit is contained in:
2026-05-06 19:41:50 +02:00
parent 9f0a40bedc
commit 7494b001a2
2 changed files with 36 additions and 7 deletions
+35 -6
View File
@@ -26,14 +26,16 @@ internal sealed class HonorificService : IDisposable
private readonly ICallGateSubscriber<object> _disposing; private readonly ICallGateSubscriber<object> _disposing;
private readonly IPluginLog _log; private readonly IPluginLog _log;
private readonly IFramework _framework;
private bool _versionWarningLogged; private bool _versionWarningLogged;
public HonorificTitleData? CurrentTitle { get; private set; } public HonorificTitleData? CurrentTitle { get; private set; }
public bool IsAvailable { get; private set; } public bool IsAvailable { get; private set; }
public (uint Major, uint Minor)? DetectedApiVersion { get; private set; } public (uint Major, uint Minor)? DetectedApiVersion { get; private set; }
public HonorificService(IDalamudPluginInterface pluginInterface, IPluginLog log) public HonorificService(IDalamudPluginInterface pluginInterface, IPluginLog log, IFramework framework)
{ {
_framework = framework;
_log = log; _log = log;
// Dalamud caches gate objects per-name for the lifetime of the // Dalamud caches gate objects per-name for the lifetime of the
@@ -41,6 +43,18 @@ internal sealed class HonorificService : IDisposable
// Honorific isn't loaded yet — the gate just won't fire. Calling // Honorific isn't loaded yet — the gate just won't fire. Calling
// InvokeFunc before Honorific is up will throw, which is why the // InvokeFunc before Honorific is up will throw, which is why the
// initial pull below is wrapped in try-catch. // initial pull below is wrapped in try-catch.
//
// Thread-context: plugin constructors run on Dalamud's plugin-loader
// thread, NOT the framework thread. Honorific's IPC handlers read
// ObjectTable.LocalPlayer (Honorific IpcProvider.cs:61), which throws
// "Not on main thread!" outside the framework thread. If Honorific is
// already loaded when HellionChat starts, a synchronous InvokeFunc
// here would surface that exception, the broad catch below would
// mark IsAvailable=false, and OnTitleChanged's `if (!IsAvailable)`
// gate would block every subsequent title update. We therefore
// schedule the initial pull onto the framework thread via
// IFramework.RunOnFrameworkThread so the IPC call sees the right
// thread context.
_apiVersion = pluginInterface _apiVersion = pluginInterface
.GetIpcSubscriber<(uint, uint)>($"{IpcNamespace}.ApiVersion"); .GetIpcSubscriber<(uint, uint)>($"{IpcNamespace}.ApiVersion");
_getLocalCharacterTitle = pluginInterface _getLocalCharacterTitle = pluginInterface
@@ -56,7 +70,7 @@ internal sealed class HonorificService : IDisposable
_ready.Subscribe(OnReady); _ready.Subscribe(OnReady);
_disposing.Subscribe(OnDisposing); _disposing.Subscribe(OnDisposing);
TryInitialPull(); _framework.RunOnFrameworkThread(TryInitialPull);
} }
public void Dispose() public void Dispose()
@@ -131,7 +145,12 @@ internal sealed class HonorificService : IDisposable
// Honorific loaded after HellionChat; redo the version check and // Honorific loaded after HellionChat; redo the version check and
// initial pull. Idempotent on purpose — Honorific can fire Ready // initial pull. Idempotent on purpose — Honorific can fire Ready
// more than once across reloads. // more than once across reloads.
TryInitialPull(); //
// Honorific's NotifyReady may dispatch from any thread, and
// TryInitialPull eventually calls IPC handlers that read
// ObjectTable.LocalPlayer — same "Not on main thread!" hazard as
// the constructor path. Schedule onto the framework thread.
_framework.RunOnFrameworkThread(TryInitialPull);
} }
private void OnDisposing() private void OnDisposing()
@@ -164,9 +183,19 @@ internal sealed class HonorificService : IDisposable
// Threading note: Dalamud fires IPC events on the framework thread and // Threading note: Dalamud fires IPC events on the framework thread and
// ImGui renders on the framework thread, so OnTitleChanged and the // ImGui renders on the framework thread, so OnTitleChanged and the
// render path that reads CurrentTitle never race. If a future change // render path that reads CurrentTitle never race — OnTitleChanged is
// moves either side onto a worker thread, switch to volatile/Interlocked // safe to keep direct (no RunOnFrameworkThread wrap needed) because
// for the CurrentTitle field. // LocalCharacterTitleChanged delivery is framework-thread by Dalamud
// contract. If a future change moves either side onto a worker thread,
// switch to volatile/Interlocked for the CurrentTitle field.
//
// The constructor's initial pull and OnReady, on the other hand, are
// explicitly scheduled via IFramework.RunOnFrameworkThread because
// they run outside that contract: the constructor executes on the
// plugin-loader thread, and Honorific's NotifyReady can dispatch from
// any thread. Both call paths eventually invoke IPC handlers that read
// ObjectTable.LocalPlayer, which throws "Not on main thread!" off the
// framework thread — see the constructor comment block for context.
// //
// Divergence from ChatTwo/Ipc/ExtraChat.cs: that file uses `volatile` // Divergence from ChatTwo/Ipc/ExtraChat.cs: that file uses `volatile`
// on its state fields out of caution. We don't, because the framework- // on its state fields out of caution. We don't, because the framework-
+1 -1
View File
@@ -443,7 +443,7 @@ public sealed class Plugin : IDalamudPlugin
// Ready/Disposing events from the target plugins are caught from // Ready/Disposing events from the target plugins are caught from
// the very first frame, even if the user's Honorific reloads // the very first frame, even if the user's Honorific reloads
// mid-session. See HellionChat/Integrations/HonorificService.cs. // mid-session. See HellionChat/Integrations/HonorificService.cs.
HonorificService = new Integrations.HonorificService(Interface, Log); HonorificService = new Integrations.HonorificService(Interface, Log, Framework);
StatusBar = new Ui.StatusBar(); StatusBar = new Ui.StatusBar();