using System; using System.Globalization; using System.IO.MemoryMappedFiles; using System.Numerics; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; namespace QuestionableCompanion.Services; public class CrossProcessIPC : IDisposable { private readonly IPluginLog log; private readonly IFramework framework; private readonly Configuration configuration; private MemoryMappedFile? mmf; private Thread? listenerThread; private bool isRunning; private const string MMF_NAME = "QSTCompanion_IPC"; private const int MMF_SIZE = 4096; private const int POLLING_INTERVAL_MS = 10; public event Action? OnHelperAvailable; public event Action? OnHelperRequested; public event Action? OnHelperDismissed; public event Action? OnChatMessageReceived; public event Action? OnCommandReceived; public event Action? OnHelperInParty; public event Action? OnHelperInDuty; public event Action? OnHelperReady; public event Action? OnRequestHelperAnnouncements; public event Action? OnChauffeurSummonRequest; public event Action? OnChauffeurReadyForPickup; public event Action? OnChauffeurArrived; public event Action? OnChauffeurZoneUpdate; public event Action? OnChauffeurMountReady; public event Action? OnChauffeurPassengerMounted; public event Action? OnHelperStatusUpdate; public event Action? OnQuesterPositionUpdate; public CrossProcessIPC(IPluginLog log, IFramework framework, Configuration configuration) { this.log = log; this.framework = framework; this.configuration = configuration; InitializeIPC(); } private void InitializeIPC() { try { mmf = MemoryMappedFile.CreateOrOpen("QSTCompanion_IPC", 4096L, MemoryMappedFileAccess.ReadWrite); isRunning = true; listenerThread = new Thread(ListenerLoop) { IsBackground = true, Name = "QSTCompanion IPC Listener" }; listenerThread.Start(); log.Information("[CrossProcessIPC] Initialized with Memory-Mapped File"); if (configuration.IsHighLevelHelper) { framework.RunOnFrameworkThread(delegate { AnnounceHelper(); }); } } catch (Exception ex) { log.Error("[CrossProcessIPC] Failed to initialize: " + ex.Message); } } private void ListenerLoop() { string lastMessage = ""; while (isRunning) { try { if (mmf == null) { break; } using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0L, 4096L, MemoryMappedFileAccess.Read)) { byte[] buffer = new byte[4096]; accessor.ReadArray(0L, buffer, 0, 4096); string message = Encoding.UTF8.GetString(buffer).TrimEnd('\0'); if (!string.IsNullOrEmpty(message) && message != lastMessage) { lastMessage = message; ProcessMessage(message); } } Thread.Sleep(10); } catch (Exception ex) { log.Error("[CrossProcessIPC] Listener error: " + ex.Message); Thread.Sleep(1000); } } } private void ProcessMessage(string message) { try { string[] parts = message.Split('|'); if (parts.Length < 2) { return; } string command = parts[0]; _003C_003Ec__DisplayClass63_0 CS_0024_003C_003E8__locals0; framework.RunOnFrameworkThread(delegate { try { string text = command; if (text != null) { switch (text.Length) { case 16: switch (text[0]) { case 'H': if (text == "HELPER_AVAILABLE" && parts.Length >= 3) { string text3 = parts[1]; if (ushort.TryParse(parts[2], out var result8)) { log.Information($"[CrossProcessIPC] Helper available: {text3}@{result8}"); this.OnHelperAvailable?.Invoke(text3, result8); } } break; case 'C': if (text == "CHAUFFEUR_SUMMON" && parts.Length >= 11) { ushort questerWorld = ushort.Parse(parts[2]); uint zoneId = uint.Parse(parts[3]); Vector3 targetPos = new Vector3(float.Parse(parts[4]), float.Parse(parts[5]), float.Parse(parts[6])); Vector3 questerPos = new Vector3(float.Parse(parts[7]), float.Parse(parts[8]), float.Parse(parts[9])); bool isAttuneAetheryte = bool.Parse(parts[10]); framework.RunOnFrameworkThread(delegate { this.OnChauffeurSummonRequest?.Invoke((string)(object)CS_0024_003C_003E8__locals0, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte); }); } break; case 'Q': if (text == "QUESTER_POSITION" && parts.Length >= 7) { string arg3 = parts[1]; if (ushort.TryParse(parts[2], out var result3) && uint.TryParse(parts[3], out var result4) && float.TryParse(parts[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var result5) && float.TryParse(parts[5], NumberStyles.Float, CultureInfo.InvariantCulture, out var result6) && float.TryParse(parts[6], NumberStyles.Float, CultureInfo.InvariantCulture, out var result7)) { Vector3 arg4 = new Vector3(result5, result6, result7); this.OnQuesterPositionUpdate?.Invoke(arg3, result3, result4, arg4); } } break; } break; case 14: switch (text[7]) { case 'R': if (text == "HELPER_REQUEST" && parts.Length >= 3) { string text12 = parts[1]; if (ushort.TryParse(parts[2], out var result14)) { log.Information($"[CrossProcessIPC] Helper request: {text12}@{result14}"); this.OnHelperRequested?.Invoke(text12, result14); } } break; case 'D': if (text == "HELPER_DISMISS") { log.Information("[CrossProcessIPC] Helper dismiss"); this.OnHelperDismissed?.Invoke(); } break; case 'I': if (text == "HELPER_IN_DUTY" && parts.Length >= 3) { string text11 = parts[1]; if (ushort.TryParse(parts[2], out var result13)) { log.Information($"[CrossProcessIPC] Helper in duty: {text11}@{result13}"); this.OnHelperInDuty?.Invoke(text11, result13); } } break; } break; case 15: switch (text[0]) { case 'H': if (text == "HELPER_IN_PARTY" && parts.Length >= 3) { string text9 = parts[1]; if (ushort.TryParse(parts[2], out var result12)) { log.Information($"[CrossProcessIPC] Helper in party: {text9}@{result12}"); this.OnHelperInParty?.Invoke(text9, result12); } } break; case 'C': if (text == "CHAUFFEUR_READY" && parts.Length >= 2) { string text8 = parts[1]; log.Information("[CrossProcessIPC] Chauffeur ready: " + text8); this.OnChauffeurReadyForPickup?.Invoke(text8); } break; } break; case 21: switch (text[10]) { case 'Z': if (text == "CHAUFFEUR_ZONE_UPDATE" && parts.Length >= 5) { string text5 = parts[1]; if (ushort.TryParse(parts[2], out var result10) && uint.TryParse(parts[3], out var result11)) { string text6 = parts[4]; log.Information($"[CrossProcessIPC] Zone update: {text5}@{result10} -> {text6} ({result11})"); this.OnChauffeurZoneUpdate?.Invoke(text5, result10, result11, text6); } } break; case 'M': if (text == "CHAUFFEUR_MOUNT_READY" && parts.Length >= 3) { string text4 = parts[1]; if (ushort.TryParse(parts[2], out var result9)) { log.Information($"[CrossProcessIPC] Chauffeur mount ready for: {text4}@{result9}"); IPluginLog pluginLog = log; DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(53, 1); defaultInterpolatedStringHandler.AppendLiteral("[CrossProcessIPC] OnChauffeurMountReady subscribers: "); Action? action = this.OnChauffeurMountReady; defaultInterpolatedStringHandler.AppendFormatted((action != null) ? action.GetInvocationList().Length : 0); pluginLog.Information(defaultInterpolatedStringHandler.ToStringAndClear()); this.OnChauffeurMountReady?.Invoke(text4, result9); } } break; } break; case 4: if (text == "CHAT" && parts.Length >= 2) { string text7 = parts[1]; log.Information("[CrossProcessIPC] Chat: " + text7); this.OnChatMessageReceived?.Invoke(text7); } break; case 7: if (text == "COMMAND" && parts.Length >= 2) { string text10 = parts[1]; log.Information("[CrossProcessIPC] Command: " + text10); this.OnCommandReceived?.Invoke(text10); } break; case 12: if (text == "HELPER_READY" && parts.Length >= 3) { string text13 = parts[1]; if (ushort.TryParse(parts[2], out var result15)) { log.Information($"[CrossProcessIPC] Helper ready: {text13}@{result15}"); this.OnHelperReady?.Invoke(text13, result15); } } break; case 28: if (text == "REQUEST_HELPER_ANNOUNCEMENTS") { log.Information("[CrossProcessIPC] Request for helper announcements received"); this.OnRequestHelperAnnouncements?.Invoke(); } break; case 17: if (text == "CHAUFFEUR_ARRIVED" && parts.Length >= 3) { string text2 = parts[1]; if (ushort.TryParse(parts[2], out var result2)) { log.Information($"[CrossProcessIPC] Chauffeur arrived for: {text2}@{result2}"); this.OnChauffeurArrived?.Invoke(text2, result2); } } break; case 27: if (text == "CHAUFFEUR_PASSENGER_MOUNTED") { log.Information("[CrossProcessIPC] Chauffeur passenger mounted signal received"); IPluginLog pluginLog2 = log; DefaultInterpolatedStringHandler defaultInterpolatedStringHandler2 = new DefaultInterpolatedStringHandler(59, 1); defaultInterpolatedStringHandler2.AppendLiteral("[CrossProcessIPC] OnChauffeurPassengerMounted subscribers: "); Action? action2 = this.OnChauffeurPassengerMounted; defaultInterpolatedStringHandler2.AppendFormatted((action2 != null) ? action2.GetInvocationList().Length : 0); pluginLog2.Information(defaultInterpolatedStringHandler2.ToStringAndClear()); this.OnChauffeurPassengerMounted?.Invoke(); } break; case 13: if (text == "HELPER_STATUS" && parts.Length >= 4) { string arg = parts[1]; if (ushort.TryParse(parts[2], out var result)) { string arg2 = parts[3]; this.OnHelperStatusUpdate?.Invoke(arg, result, arg2); } } break; } } } catch (Exception ex2) { log.Error("[CrossProcessIPC] Error in event handler: " + ex2.Message); } }); } catch (Exception ex) { log.Error("[CrossProcessIPC] Error processing message: " + ex.Message); } } private void SendMessage(string message) { try { if (mmf == null) { return; } using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0L, 4096L, MemoryMappedFileAccess.Write); byte[] buffer = Encoding.UTF8.GetBytes(message); if (buffer.Length > 4095) { log.Warning($"[CrossProcessIPC] Message too large: {buffer.Length} bytes"); } else { byte[] clearBuffer = new byte[4096]; accessor.WriteArray(0L, clearBuffer, 0, 4096); accessor.WriteArray(0L, buffer, 0, buffer.Length); } } catch (Exception ex) { log.Error("[CrossProcessIPC] Failed to send message: " + ex.Message); } } public void AnnounceHelper() { if (configuration.IsHighLevelHelper) { IPlayerCharacter localPlayer = Plugin.ClientState?.LocalPlayer; if (localPlayer != null) { string name = localPlayer.Name.ToString(); ushort worldId = (ushort)localPlayer.HomeWorld.RowId; SendMessage($"HELPER_AVAILABLE|{name}|{worldId}"); log.Information($"[CrossProcessIPC] Announced as helper: {name}@{worldId}"); } } } public void RequestHelper(string characterName, ushort worldId) { SendMessage($"HELPER_REQUEST|{characterName}|{worldId}"); log.Information($"[CrossProcessIPC] Requested helper: {characterName}@{worldId}"); } public void DismissHelper() { SendMessage("HELPER_DISMISS"); log.Information("[CrossProcessIPC] Dismissed helper"); } public void SendChatMessage(string message) { SendMessage("CHAT|" + message); log.Information("[CrossProcessIPC] Chat: " + message); } public void SendCommand(string command) { SendMessage("COMMAND|" + command); log.Information("[CrossProcessIPC] Command: " + command); } public void NotifyHelperInParty(string name, ushort worldId) { SendMessage($"HELPER_IN_PARTY|{name}|{worldId}"); log.Information($"[CrossProcessIPC] Notified: Helper in party {name}@{worldId}"); } public void NotifyHelperInDuty(string name, ushort worldId) { SendMessage($"HELPER_IN_DUTY|{name}|{worldId}"); log.Information($"[CrossProcessIPC] Notified: Helper in duty {name}@{worldId}"); } public void NotifyHelperReady(string name, ushort worldId) { SendMessage($"HELPER_READY|{name}|{worldId}"); log.Information($"[CrossProcessIPC] Notified: Helper ready {name}@{worldId}"); } public void RequestHelperAnnouncements() { SendMessage("REQUEST_HELPER_ANNOUNCEMENTS"); log.Information("[CrossProcessIPC] Requesting helper announcements from all clients"); } public void SendChauffeurSummonRequest(string questerName, ushort questerWorld, uint zoneId, Vector3 targetPos, Vector3 questerPos, bool isAttuneAetheryte) { SendMessage($"CHAUFFEUR_SUMMON|{questerName}|{questerWorld}|{zoneId}|{targetPos.X}|{targetPos.Y}|{targetPos.Z}|{questerPos.X}|{questerPos.Y}|{questerPos.Z}|{isAttuneAetheryte}"); log.Information($"[CrossProcessIPC] Chauffeur summon: {questerName}@{questerWorld} zone {zoneId} quester@({questerPos.X:F2},{questerPos.Y:F2},{questerPos.Z:F2}) AttuneAetheryte={isAttuneAetheryte}"); } public void SendChauffeurMountReady(string questerName, ushort questerWorld) { SendMessage($"CHAUFFEUR_MOUNT_READY|{questerName}|{questerWorld}"); log.Information($"[CrossProcessIPC] Chauffeur mount ready for RidePillion: {questerName}@{questerWorld}"); } public void SendChauffeurPassengerMounted() { SendMessage("CHAUFFEUR_PASSENGER_MOUNTED"); log.Debug("[CrossProcessIPC] Sent: CHAUFFEUR_PASSENGER_MOUNTED"); } public void SendChauffeurReadyForPickup(string helperName) { SendMessage("CHAUFFEUR_READY|" + helperName); log.Information("[CrossProcessIPC] Chauffeur ready: " + helperName); } public void SendChauffeurArrived(string questerName, ushort questerWorld) { SendMessage($"CHAUFFEUR_ARRIVED|{questerName}|{questerWorld}"); log.Information($"[CrossProcessIPC] Chauffeur arrived for: {questerName}@{questerWorld}"); } public void SendChauffeurZoneUpdate(string characterName, ushort worldId, uint zoneId, string zoneName) { SendMessage($"CHAUFFEUR_ZONE_UPDATE|{characterName}|{worldId}|{zoneId}|{zoneName}"); log.Information($"[CrossProcessIPC] Zone update: {characterName}@{worldId} -> {zoneName} ({zoneId})"); } public void BroadcastHelperStatus(string helperName, ushort helperWorld, string status) { SendMessage($"HELPER_STATUS|{helperName}|{helperWorld}|{status}"); } public void BroadcastQuesterPosition(string questerName, ushort questerWorld, uint zoneId, Vector3 position) { SendMessage($"QUESTER_POSITION|{questerName}|{questerWorld}|{zoneId}|{position.X.ToString(CultureInfo.InvariantCulture)}|{position.Y.ToString(CultureInfo.InvariantCulture)}|{position.Z.ToString(CultureInfo.InvariantCulture)}"); } public void Dispose() { isRunning = false; listenerThread?.Join(1000); mmf?.Dispose(); log.Information("[CrossProcessIPC] Disposed"); } }