511 lines
14 KiB
C#
511 lines
14 KiB
C#
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<List<ulong>>? getRegisteredCIDsSubscriber;
|
|
|
|
private ICallGateSubscriber<ulong, object>? getOfflineCharacterDataSubscriber;
|
|
|
|
private ICallGateProvider<string, object>? relogProvider;
|
|
|
|
private ICallGateSubscriber<bool>? getMultiModeEnabledSubscriber;
|
|
|
|
private ICallGateProvider<bool, object>? setMultiModeEnabledProvider;
|
|
|
|
private Dictionary<ulong, string> characterCache = new Dictionary<ulong, string>();
|
|
|
|
private HashSet<ulong> unknownCIDs = new HashSet<ulong>();
|
|
|
|
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<List<ulong>>("AutoRetainer.GetRegisteredCIDs");
|
|
getOfflineCharacterDataSubscriber = pluginInterface.GetIpcSubscriber<ulong, object>("AutoRetainer.GetOfflineCharacterData");
|
|
try
|
|
{
|
|
relogProvider = pluginInterface.GetIpcProvider<string, object>("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<bool>("AutoRetainer.GetMultiModeEnabled");
|
|
setMultiModeEnabledProvider = pluginInterface.GetIpcProvider<bool, object>("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<ulong> 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<string> 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<string>();
|
|
}
|
|
try
|
|
{
|
|
List<ulong> cids = getRegisteredCIDsSubscriber.InvokeFunc();
|
|
if (cids == null || cids.Count == 0)
|
|
{
|
|
log.Warning("[AutoRetainerIPC] No CIDs returned from AutoRetainer");
|
|
return new List<string>();
|
|
}
|
|
List<string> characters = new List<string>();
|
|
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<string>();
|
|
}
|
|
}
|
|
|
|
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;
|
|
return resolvedName;
|
|
}
|
|
}
|
|
if (data is JToken jToken)
|
|
{
|
|
resolvedName = ParseJTokenCharacterData(jToken, cid);
|
|
if (!string.IsNullOrEmpty(resolvedName))
|
|
{
|
|
characterCache[cid] = resolvedName;
|
|
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>();
|
|
string world = worldToken.Value<string>();
|
|
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<string>();
|
|
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");
|
|
}
|
|
}
|