using Dalamud.Plugin; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Linq; namespace LightlessSync.Interop.Ipc.Framework; public enum IpcConnectionState { Unknown = 0, MissingPlugin = 1, VersionMismatch = 2, PluginDisabled = 3, NotReady = 4, Available = 5, Error = 6, } public sealed record IpcServiceDescriptor(string InternalName, string DisplayName, Version MinimumVersion) { public override string ToString() => $"{DisplayName} (>= {MinimumVersion})"; } public interface IIpcService : IDisposable { IpcServiceDescriptor Descriptor { get; } IpcConnectionState State { get; } IDalamudPluginInterface PluginInterface { get; } bool APIAvailable { get; } void CheckAPI(); } public interface IIpcInterop : IDisposable { string Name { get; } void OnConnectionStateChanged(IpcConnectionState state); } public abstract class IpcInteropBase : IIpcInterop { protected IpcInteropBase(ILogger logger) { Logger = logger; } protected ILogger Logger { get; } protected IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown; protected bool IsAvailable => State == IpcConnectionState.Available; public abstract string Name { get; } public void OnConnectionStateChanged(IpcConnectionState state) { if (State == state) { return; } var previous = State; State = state; HandleStateChange(previous, state); } protected abstract void HandleStateChange(IpcConnectionState previous, IpcConnectionState current); public virtual void Dispose() { } } public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcService { private readonly List _interops = new(); protected IpcServiceBase( ILogger logger, LightlessMediator mediator, IDalamudPluginInterface pluginInterface, IpcServiceDescriptor descriptor) : base(logger, mediator) { PluginInterface = pluginInterface; Descriptor = descriptor; } protected IDalamudPluginInterface PluginInterface { get; } IDalamudPluginInterface IIpcService.PluginInterface => PluginInterface; protected IpcServiceDescriptor Descriptor { get; } IpcServiceDescriptor IIpcService.Descriptor => Descriptor; public IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown; public bool APIAvailable => State == IpcConnectionState.Available; public virtual void CheckAPI() { var newState = EvaluateState(); UpdateState(newState); } protected virtual IpcConnectionState EvaluateState() { try { var plugin = PluginInterface.InstalledPlugins .Where(p => string.Equals(p.InternalName, Descriptor.InternalName, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(p => p.IsLoaded) .FirstOrDefault(); if (plugin == null) { return IpcConnectionState.MissingPlugin; } if (plugin.Version < Descriptor.MinimumVersion) { return IpcConnectionState.VersionMismatch; } if (!IsPluginEnabled(plugin)) { return IpcConnectionState.PluginDisabled; } if (!IsPluginReady()) { return IpcConnectionState.NotReady; } return IpcConnectionState.Available; } catch (Exception ex) { Logger.LogDebug(ex, "Failed to evaluate IPC state for {Service}", Descriptor.DisplayName); return IpcConnectionState.Error; } } protected virtual bool IsPluginEnabled(IExposedPlugin plugin) => plugin.IsLoaded; protected virtual bool IsPluginReady() => true; protected TInterop RegisterInterop(TInterop interop) where TInterop : IIpcInterop { _interops.Add(interop); interop.OnConnectionStateChanged(State); return interop; } private void UpdateState(IpcConnectionState newState) { if (State == newState) { return; } var previous = State; State = newState; OnConnectionStateChanged(previous, newState); foreach (var interop in _interops) { interop.OnConnectionStateChanged(newState); } } protected virtual void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) { Logger.LogTrace("{Service} IPC state transitioned from {Previous} to {Current}", Descriptor.DisplayName, previous, current); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!disposing) { return; } for (var i = _interops.Count - 1; i >= 0; --i) { _interops[i].Dispose(); } _interops.Clear(); } }