using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Services; using Newtonsoft.Json.Linq; namespace QuestionableCompanion.Services; public class AutoRetainerIPC : IDisposable { private readonly IDalamudPluginInterface pluginInterface; private readonly IPluginLog log; private readonly IClientState clientState; private readonly ICommandManager commandManager; private readonly IFramework framework; private ICallGateSubscriber>? getRegisteredCIDsSubscriber; private ICallGateSubscriber? getOfflineCharacterDataSubscriber; private ICallGateProvider? relogProvider; private ICallGateSubscriber? getMultiModeEnabledSubscriber; private ICallGateProvider? setMultiModeEnabledProvider; private Dictionary characterCache = new Dictionary(); private HashSet unknownCIDs = new HashSet(); private bool subscribersInitialized; private DateTime lastAvailabilityCheck = DateTime.MinValue; private const int AvailabilityCheckCooldownSeconds = 5; public bool IsAvailable { get; private set; } public AutoRetainerIPC(IDalamudPluginInterface pluginInterface, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework) { this.pluginInterface = pluginInterface; this.log = log; this.clientState = clientState; this.commandManager = commandManager; this.framework = framework; InitializeIPC(); } public void ClearCache() { characterCache.Clear(); unknownCIDs.Clear(); log.Information("[AutoRetainerIPC] Cache cleared"); } private void InitializeIPC() { try { getRegisteredCIDsSubscriber = null; getOfflineCharacterDataSubscriber = null; relogProvider = null; getMultiModeEnabledSubscriber = null; setMultiModeEnabledProvider = null; IsAvailable = false; getRegisteredCIDsSubscriber = pluginInterface.GetIpcSubscriber>("AutoRetainer.GetRegisteredCIDs"); getOfflineCharacterDataSubscriber = pluginInterface.GetIpcSubscriber("AutoRetainer.GetOfflineCharacterData"); try { relogProvider = pluginInterface.GetIpcProvider("AutoRetainer.Relog"); log.Debug("[AutoRetainerIPC] Relog IPC provider initialized"); } catch (Exception ex) { log.Debug("[AutoRetainerIPC] Failed to initialize Relog provider: " + ex.Message); } try { getMultiModeEnabledSubscriber = pluginInterface.GetIpcSubscriber("AutoRetainer.GetMultiModeEnabled"); setMultiModeEnabledProvider = pluginInterface.GetIpcProvider("AutoRetainer.SetMultiModeEnabled"); log.Debug("[AutoRetainerIPC] Multi-Mode IPC initialized"); } catch (Exception ex2) { log.Debug("[AutoRetainerIPC] Failed to initialize Multi-Mode IPC: " + ex2.Message); } subscribersInitialized = true; log.Debug("[AutoRetainerIPC] IPC subscribers initialized (lazy-loading enabled)"); } catch (Exception ex3) { IsAvailable = false; subscribersInitialized = false; log.Error("[AutoRetainerIPC] Failed to initialize subscribers: " + ex3.Message); } } private bool TryEnsureAvailable() { if (IsAvailable) { return true; } if (!subscribersInitialized) { return false; } DateTime now = DateTime.Now; if ((now - lastAvailabilityCheck).TotalSeconds < 5.0) { return false; } lastAvailabilityCheck = now; try { if (getRegisteredCIDsSubscriber == null) { return false; } List testCids = getRegisteredCIDsSubscriber.InvokeFunc(); if (!IsAvailable) { IsAvailable = true; log.Information($"[AutoRetainerIPC] ✅ AutoRetainer is now available ({testCids?.Count ?? 0} characters)"); } return true; } catch (Exception ex) { log.Debug("[AutoRetainerIPC] AutoRetainer not yet available: " + ex.Message); IsAvailable = false; return false; } } public bool TryReinitialize() { log.Information("[AutoRetainerIPC] Manual IPC reinitialization requested"); lastAvailabilityCheck = DateTime.MinValue; bool num = TryEnsureAvailable(); if (num) { log.Information("[AutoRetainerIPC] IPC reinitialization successful"); return num; } log.Warning("[AutoRetainerIPC] IPC still unavailable after reinitialization attempt"); return num; } public List GetRegisteredCharacters() { log.Debug("[AutoRetainerIPC] GetRegisteredCharacters called"); TryEnsureAvailable(); if (!IsAvailable || getRegisteredCIDsSubscriber == null) { log.Warning("[AutoRetainerIPC] Cannot get characters - IPC not available"); log.Warning($"[AutoRetainerIPC] IsAvailable: {IsAvailable}, Subscriber: {getRegisteredCIDsSubscriber != null}"); return new List(); } try { List cids = getRegisteredCIDsSubscriber.InvokeFunc(); if (cids == null || cids.Count == 0) { log.Warning("[AutoRetainerIPC] No CIDs returned from AutoRetainer"); return new List(); } List characters = new List(); foreach (ulong cid in cids) { string charName = GetCharacterNameFromCID(cid); if (!string.IsNullOrEmpty(charName)) { characters.Add(charName); continue; } log.Debug($"[AutoRetainerIPC] Could not resolve name for CID: {cid}"); } if (characters.Count == 0) { log.Warning("[AutoRetainerIPC] No character names could be resolved from CIDs"); } return characters; } catch (Exception ex) { log.Error("[AutoRetainerIPC] GetRegisteredCharacters failed: " + ex.Message); log.Error("[AutoRetainerIPC] Stack trace: " + ex.StackTrace); return new List(); } } private string GetCharacterNameFromCID(ulong cid) { if (characterCache.TryGetValue(cid, out string cachedName)) { if (cachedName.Contains("@")) { return cachedName; } log.Debug($"[AutoRetainerIPC] Removing invalid cache entry for CID {cid}: '{cachedName}'"); characterCache.Remove(cid); } if (unknownCIDs.Contains(cid)) { return $"Unknown (CID: {cid})"; } if (getOfflineCharacterDataSubscriber == null) { log.Debug("[AutoRetainerIPC] OfflineCharacterData subscriber is null"); return string.Empty; } try { object data = getOfflineCharacterDataSubscriber.InvokeFunc(cid); if (data == null) { if (!unknownCIDs.Contains(cid)) { log.Warning($"[AutoRetainerIPC] No data returned for CID {cid}"); unknownCIDs.Add(cid); } return $"Unknown (CID: {cid})"; } string resolvedName = null; FieldInfo nameField = data.GetType().GetField("Name"); FieldInfo worldField = data.GetType().GetField("World"); if (nameField != null && worldField != null) { string name = nameField.GetValue(data)?.ToString(); string world = worldField.GetValue(data)?.ToString(); log.Warning($"[AutoRetainerIPC] Field values for CID {cid} - Name: '{name}', World: '{world}'"); if (!string.IsNullOrEmpty(name) && name != "Unknown") { if (string.IsNullOrEmpty(world) && clientState.IsLoggedIn && clientState.LocalPlayer != null && clientState.LocalPlayer.Name.ToString() == name) { world = clientState.LocalPlayer.HomeWorld.Value.Name.ToString(); log.Information("[AutoRetainerIPC] Resolved world from ClientState for " + name + ": " + world); } if (!string.IsNullOrEmpty(world)) { resolvedName = name + "@" + world; characterCache[cid] = resolvedName; return resolvedName; } log.Warning($"[AutoRetainerIPC] World is empty for CID {cid}, cannot create full name"); } else { log.Warning($"[AutoRetainerIPC] Name is empty/invalid for CID {cid}"); } } PropertyInfo nameProperty = data.GetType().GetProperty("Name"); PropertyInfo worldProperty = data.GetType().GetProperty("World"); if (nameProperty != null && worldProperty != null) { string name2 = nameProperty.GetValue(data)?.ToString(); string world2 = worldProperty.GetValue(data)?.ToString(); if (!string.IsNullOrEmpty(name2) && !string.IsNullOrEmpty(world2) && name2 != "Unknown") { resolvedName = name2 + "@" + world2; characterCache[cid] = resolvedName; log.Debug($"[AutoRetainerIPC] CID {cid} resolved to {resolvedName} (via properties)"); return resolvedName; } } if (data is JToken jToken) { resolvedName = ParseJTokenCharacterData(jToken, cid); if (!string.IsNullOrEmpty(resolvedName)) { characterCache[cid] = resolvedName; log.Debug($"[AutoRetainerIPC] CID {cid} resolved to {resolvedName} (via JSON)"); return resolvedName; } } if (!unknownCIDs.Contains(cid)) { log.Warning($"[AutoRetainerIPC] Could not resolve name for CID {cid}"); LogDataStructure(data, cid); unknownCIDs.Add(cid); } return $"Unknown (CID: {cid})"; } catch (Exception ex) { if (!unknownCIDs.Contains(cid)) { log.Warning($"[AutoRetainerIPC] Exception resolving CID {cid}: {ex.Message}"); log.Debug("[AutoRetainerIPC] Stack trace: " + ex.StackTrace); unknownCIDs.Add(cid); } return $"Unknown (CID: {cid})"; } } private string? ParseJTokenCharacterData(JToken jToken, ulong cid) { try { JToken nameToken = jToken.SelectToken("Name") ?? jToken.SelectToken("name") ?? jToken.SelectToken("Value.Name"); JToken worldToken = jToken.SelectToken("World") ?? jToken.SelectToken("world") ?? jToken.SelectToken("Value.World"); if (nameToken != null && worldToken != null) { string name = nameToken.Value(); string world = worldToken.Value(); if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(world)) { return name + "@" + world; } if (!string.IsNullOrEmpty(name)) { log.Warning($"[AutoRetainerIPC] JSON has Name but World is empty for CID {cid}"); } } string[] array = new string[4] { "NameWithWorld", "nameWithWorld", "[\"NameWithWorld\"]", "Value.NameWithWorld" }; foreach (string path in array) { if (string.IsNullOrEmpty(path)) { continue; } JToken token = jToken.SelectToken(path); if (token != null && token.Type == JTokenType.String) { string value = token.Value(); if (!string.IsNullOrEmpty(value) && value.Contains("@")) { log.Information("[AutoRetainerIPC] Found name via JSON path '" + path + "': " + value); return value; } } } } catch (Exception ex) { log.Warning($"[AutoRetainerIPC] Error parsing JToken for CID {cid}: {ex.Message}"); } return null; } private void LogDataStructure(object data, ulong cid) { try { if (data is JToken jToken) { log.Debug($"[AutoRetainerIPC] JSON structure for CID {cid}:"); log.Debug(jToken.ToString()); return; } PropertyInfo[] properties = data.GetType().GetProperties(); log.Debug($"[AutoRetainerIPC] Object structure for CID {cid}:"); foreach (PropertyInfo prop in properties.Take(10)) { try { object value = prop.GetValue(data); log.Debug($" {prop.Name} = {value ?? "(null)"}"); } catch { log.Debug(" " + prop.Name + " = (error reading value)"); } } } catch { } } public string? GetCurrentCharacter() { try { string result = null; framework.RunOnFrameworkThread(delegate { try { if (!clientState.IsLoggedIn) { result = null; } else if (clientState.LocalPlayer == null) { result = null; } else { string text = clientState.LocalPlayer.Name.ToString(); string text2 = clientState.LocalPlayer.HomeWorld.Value.Name.ToString(); if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(text2)) { result = null; } else { result = text + "@" + text2; } } } catch (Exception ex2) { log.Debug("[AutoRetainerIPC] GetCurrentCharacter inner failed: " + ex2.Message); result = null; } }).Wait(); return result; } catch (Exception ex) { log.Debug("[AutoRetainerIPC] GetCurrentCharacter failed: " + ex.Message); return null; } } public bool SwitchCharacter(string characterNameWithWorld) { if (string.IsNullOrEmpty(characterNameWithWorld)) { log.Warning("[AutoRetainerIPC] Character name is null or empty"); return false; } TryEnsureAvailable(); if (!IsAvailable) { log.Warning("[AutoRetainerIPC] AutoRetainer not available"); return false; } try { log.Information("[AutoRetainerIPC] Requesting relog to: " + characterNameWithWorld); string command = "/ays relog " + characterNameWithWorld; bool success = false; framework.RunOnFrameworkThread(delegate { try { commandManager.ProcessCommand(command); success = true; log.Information("[AutoRetainerIPC] Relog command executed: " + command); } catch (Exception ex2) { log.Error("[AutoRetainerIPC] Failed to execute relog command: " + ex2.Message); success = false; } }).Wait(); return success; } catch (Exception ex) { log.Error("[AutoRetainerIPC] Failed to switch character: " + ex.Message); return false; } } public bool GetMultiModeEnabled() { TryEnsureAvailable(); if (!IsAvailable || getMultiModeEnabledSubscriber == null) { log.Debug("[AutoRetainerIPC] Multi-Mode IPC not available"); return false; } try { return getMultiModeEnabledSubscriber.InvokeFunc(); } catch (Exception ex) { log.Error("[AutoRetainerIPC] GetMultiModeEnabled failed: " + ex.Message); return false; } } public bool SetMultiModeEnabled(bool enabled) { TryEnsureAvailable(); if (!IsAvailable || setMultiModeEnabledProvider == null) { log.Warning("[AutoRetainerIPC] Multi-Mode IPC not available"); return false; } try { setMultiModeEnabledProvider.SendMessage(enabled); log.Information($"[AutoRetainerIPC] Multi-Mode set to: {enabled}"); return true; } catch (Exception ex) { log.Error("[AutoRetainerIPC] SetMultiModeEnabled failed: " + ex.Message); return false; } } public void Dispose() { IsAvailable = false; characterCache.Clear(); unknownCIDs.Clear(); log.Information("[AutoRetainerIPC] Service disposed"); } }