using System; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Services; namespace QuestionableCompanion.Services; public class LifestreamIPC : IDisposable { private readonly IPluginLog log; private readonly IDalamudPluginInterface pluginInterface; private ICallGateSubscriber? isBusySubscriber; private ICallGateSubscriber? changeWorldSubscriber; private ICallGateSubscriber? changeWorldByIdSubscriber; private ICallGateSubscriber? abortSubscriber; private bool _isAvailable; private bool _ipcInitialized; private DateTime lastAvailabilityCheck = DateTime.MinValue; private const int AvailabilityCheckCooldownSeconds = 5; private bool hasPerformedInitialCheck; public bool IsAvailable { get { return _isAvailable; } private set { _isAvailable = value; } } public LifestreamIPC(IPluginLog log, IDalamudPluginInterface pluginInterface) { this.log = log; this.pluginInterface = pluginInterface; } private void InitializeIPC() { if (_ipcInitialized) { return; } try { isBusySubscriber = pluginInterface.GetIpcSubscriber("Lifestream.IsBusy"); changeWorldSubscriber = pluginInterface.GetIpcSubscriber("Lifestream.ChangeWorld"); changeWorldByIdSubscriber = pluginInterface.GetIpcSubscriber("Lifestream.ChangeWorldById"); abortSubscriber = pluginInterface.GetIpcSubscriber("Lifestream.Abort"); _ipcInitialized = true; log.Debug("[LifestreamIPC] IPC subscribers initialized (lazy-loading enabled)"); } catch (Exception ex) { log.Error("[LifestreamIPC] Failed to initialize subscribers: " + ex.Message); _isAvailable = false; _ipcInitialized = false; } } private bool TryEnsureAvailable(bool forceCheck = false) { if (_isAvailable) { return true; } if (!_ipcInitialized) { InitializeIPC(); } if (!_ipcInitialized) { return false; } DateTime now = DateTime.Now; if (!forceCheck && hasPerformedInitialCheck && (now - lastAvailabilityCheck).TotalSeconds < 5.0) { log.Debug($"[LifestreamIPC] Cooldown active - skipping check (last check: {(now - lastAvailabilityCheck).TotalSeconds:F1}s ago)"); return false; } if (forceCheck) { log.Information("[LifestreamIPC] FORCED availability check requested"); } lastAvailabilityCheck = now; hasPerformedInitialCheck = true; try { if (isBusySubscriber == null) { log.Debug("[LifestreamIPC] isBusySubscriber is NULL - cannot check availability"); _isAvailable = false; return false; } log.Debug("[LifestreamIPC] Attempting to invoke Lifestream.IsBusy()..."); bool testBusy = isBusySubscriber.InvokeFunc(); if (!_isAvailable) { _isAvailable = true; log.Information($"[LifestreamIPC] Lifestream is now available (Busy: {testBusy})"); } else { log.Debug($"[LifestreamIPC] Lifestream still available (Busy: {testBusy})"); } return true; } catch (Exception ex) { if (!hasPerformedInitialCheck) { log.Warning("[LifestreamIPC] First availability check FAILED: " + ex.GetType().Name + ": " + ex.Message); } else { log.Debug("[LifestreamIPC] Lifestream not yet available: " + ex.Message); } _isAvailable = false; return false; } } public bool IsBusy() { TryEnsureAvailable(); if (!_isAvailable || isBusySubscriber == null) { return false; } try { return isBusySubscriber.InvokeFunc(); } catch (Exception ex) { log.Error("[LifestreamIPC] Error checking busy status: " + ex.Message); return false; } } public bool ForceCheckAvailability() { log.Information("[LifestreamIPC] ========================================"); log.Information("[LifestreamIPC] === FORCING AVAILABILITY CHECK ==="); log.Information("[LifestreamIPC] ========================================"); bool result = TryEnsureAvailable(forceCheck: true); log.Information($"[LifestreamIPC] Force check result: {result}"); return result; } public bool ChangeWorld(string worldName) { TryEnsureAvailable(); log.Information("[LifestreamIPC] ========================================"); log.Information("[LifestreamIPC] === CHANGE WORLD REQUEST ==="); log.Information("[LifestreamIPC] ========================================"); log.Information("[LifestreamIPC] Target World: '" + worldName + "'"); log.Information($"[LifestreamIPC] IsAvailable: {_isAvailable}"); log.Information($"[LifestreamIPC] changeWorldSubscriber != null: {changeWorldSubscriber != null}"); if (!_isAvailable || changeWorldSubscriber == null) { log.Error("[LifestreamIPC] CANNOT CHANGE WORLD - Lifestream not available!"); log.Error("[LifestreamIPC] Make sure Lifestream plugin is installed and enabled!"); return false; } try { log.Information("[LifestreamIPC] Invoking Lifestream.ChangeWorld('" + worldName + "')..."); bool num = changeWorldSubscriber.InvokeFunc(worldName); if (num) { log.Information("[LifestreamIPC] ========================================"); log.Information("[LifestreamIPC] WORLD CHANGE ACCEPTED: " + worldName); log.Information("[LifestreamIPC] ========================================"); } else { log.Warning("[LifestreamIPC] ========================================"); log.Warning("[LifestreamIPC] WORLD CHANGE REJECTED: " + worldName); log.Warning("[LifestreamIPC] ========================================"); log.Warning("[LifestreamIPC] Possible reasons:"); log.Warning("[LifestreamIPC] - Lifestream is busy"); log.Warning("[LifestreamIPC] - World name is invalid"); log.Warning("[LifestreamIPC] - Cannot visit this world"); } return num; } catch (Exception ex) { log.Error("[LifestreamIPC] ========================================"); log.Error("[LifestreamIPC] ERROR REQUESTING WORLD CHANGE!"); log.Error("[LifestreamIPC] ========================================"); log.Error("[LifestreamIPC] Error: " + ex.Message); log.Error("[LifestreamIPC] Stack: " + ex.StackTrace); return false; } } public bool ChangeWorldById(uint worldId) { TryEnsureAvailable(); if (!_isAvailable || changeWorldByIdSubscriber == null) { log.Warning("[LifestreamIPC] Lifestream not available for world change"); return false; } try { log.Information($"[LifestreamIPC] Requesting world change to ID: {worldId}"); bool num = changeWorldByIdSubscriber.InvokeFunc(worldId); if (num) { log.Information($"[LifestreamIPC] World change request accepted for ID: {worldId}"); } else { log.Warning($"[LifestreamIPC] World change request rejected for ID: {worldId}"); } return num; } catch (Exception ex) { log.Error("[LifestreamIPC] Error requesting world change by ID: " + ex.Message); return false; } } public void Abort() { TryEnsureAvailable(); if (!_isAvailable || abortSubscriber == null) { return; } try { abortSubscriber.InvokeAction(); log.Information("[LifestreamIPC] Abort request sent to Lifestream"); } catch (Exception ex) { log.Error("[LifestreamIPC] Error aborting Lifestream: " + ex.Message); } } public void Dispose() { log.Information("[LifestreamIPC] Service disposed"); } }