using Dalamud.Plugin.Ipc; using Microsoft.Extensions.Logging; namespace HellionChat.Ipc; public sealed class ExtraChat : IDisposable { private readonly ILogger _logger; #pragma warning disable CS0649 // Assigned through IPC [Serializable] private struct OverrideInfo { public string? Channel; public ushort UiColour; public uint Rgba; } #pragma warning restore CS0649 private ICallGateSubscriber OverrideChannelGate { get; } private ICallGateSubscriber< Dictionary, Dictionary > ChannelCommandColoursGate { get; } private ICallGateSubscriber< Dictionary, Dictionary > ChannelNamesGate { get; } internal (string, uint)? ChannelOverride { get; set; } // volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these. // Reference assignment is atomic on x64, but the barrier ensures visibility // across threads (especially Mono/Wine). See AUDIT-2026-05-05 [SEC-01]. private volatile Dictionary ChannelCommandColoursInternal = new(); internal IReadOnlyDictionary ChannelCommandColours => ChannelCommandColoursInternal; private volatile Dictionary ChannelNamesInternal = new(); internal IReadOnlyDictionary ChannelNames => ChannelNamesInternal; internal ExtraChat(ILogger logger) { _logger = logger; OverrideChannelGate = Plugin.Interface.GetIpcSubscriber( "ExtraChat.OverrideChannelColour" ); ChannelCommandColoursGate = Plugin.Interface.GetIpcSubscriber< Dictionary, Dictionary >("ExtraChat.ChannelCommandColours"); ChannelNamesGate = Plugin.Interface.GetIpcSubscriber< Dictionary, Dictionary >("ExtraChat.ChannelNames"); OverrideChannelGate.Subscribe(OnOverrideChannel); ChannelCommandColoursGate.Subscribe(OnChannelCommandColours); ChannelNamesGate.Subscribe(OnChannelNames); try { ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!); ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!); } catch (Exception ex) { // ExtraChat is optional; IPC failure is normal when the plugin isn't loaded. _logger.LogTrace(ex, "ExtraChat IPC initial state query failed (peer not loaded?)"); } } public void Dispose() { OverrideChannelGate.Unsubscribe(OnOverrideChannel); ChannelCommandColoursGate.Unsubscribe(OnChannelCommandColours); ChannelNamesGate.Unsubscribe(OnChannelNames); } private void OnOverrideChannel(OverrideInfo info) { ChannelOverride = info.Channel == null ? null : (info.Channel, info.Rgba); } private void OnChannelCommandColours(Dictionary obj) => ChannelCommandColoursInternal = obj; private void OnChannelNames(Dictionary obj) => ChannelNamesInternal = obj; }