qstbak/QuestionableCompanion/QuestionableCompanion.Services/CrossProcessIPC.cs
2025-12-04 04:39:08 +10:00

514 lines
16 KiB
C#

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<string, ushort>? OnHelperAvailable;
public event Action<string, ushort>? OnHelperRequested;
public event Action? OnHelperDismissed;
public event Action<string>? OnChatMessageReceived;
public event Action<string>? OnCommandReceived;
public event Action<string, ushort>? OnHelperInParty;
public event Action<string, ushort>? OnHelperInDuty;
public event Action<string, ushort>? OnHelperReady;
public event Action? OnRequestHelperAnnouncements;
public event Action<string, ushort, uint, Vector3, Vector3, bool>? OnChauffeurSummonRequest;
public event Action<string>? OnChauffeurReadyForPickup;
public event Action<string, ushort>? OnChauffeurArrived;
public event Action<string, ushort, uint, string>? OnChauffeurZoneUpdate;
public event Action<string, ushort>? OnChauffeurMountReady;
public event Action? OnChauffeurPassengerMounted;
public event Action<string, ushort, string>? OnHelperStatusUpdate;
public event Action<string, ushort, uint, Vector3>? 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<string, ushort>? 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");
}
}